﻿<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3c.org/TR/1999/REC-html401-19991224/loose.dtd">
<!-- saved from url=(0053)http://msdn.microsoft.com/zh-cn/library/aa480686.aspx -->
<HTML xmlns="http://www.w3.org/1999/xhtml"><HEAD id=ctl00_Head1><TITLE>Microsoft Sudoku：优化 UMPC 应用程序的点触和手写操作</TITLE>
<META http-equiv=Content-Type content="text/html; charset=utf-8"><LINK 
id=ctl00_HeaderLink1 
href="Microsoft Sudoku：优化 UMPC 应用程序的点触和手写操作.files/Global-bn20091211.css" 
type=text/css rel=stylesheet><LINK id=ctl00_HeaderLink2 
href="Microsoft Sudoku：优化 UMPC 应用程序的点触和手写操作.files/Library-bn20091211.css" 
type=text/css rel=stylesheet>
<META 
content=http://msdn.microsoft.com/zh-cn/library(d=default)/aa480686(l=zh-cn,v=MSDN.10).aspx 
name=NormalizedUrl>
<META 
content=http://msdn.microsoft.com/zh-cn/library(d=default)/aa480686(l=zh-cn,v=MSDN.10).aspx 
name=VotingContextUrl>
<META content=NOINDEX,NOFOLLOW name=ROBOTS>
<META content="1AE42B56-4:06:41 AM" name=MN>
<META content=zh-cn name=ms.locale>
<META content=aa480686 name=Search.ShortId><!-- Looking for metadata? Use the robot view instead http://currentsite/currenturl(robot).aspx -->
<META content=/zh-cn/library(d=default)/aa480686(l=zh-cn,v=MSDN.10).aspx 
name=DCS.dcsuri><LINK 
href="Microsoft Sudoku：优化 UMPC 应用程序的点触和手写操作.files/CAIV0LM3.css" type=text/css 
rel=stylesheet></LINK><LINK media=print 
href="/zh-cn/library/aa480686(printer).aspx" rel=alternate>
<META content="MSHTML 6.00.2900.5897" name=GENERATOR></HEAD>
<BODY id=ctl00_Mtps_body dir=ltr>
<FORM id=aspnetForm name=aspnetForm action=aa480686.aspx method=post>
<DIV><INPUT id=__EVENTTARGET type=hidden name=__EVENTTARGET> <INPUT 
id=__EVENTARGUMENT type=hidden name=__EVENTARGUMENT> <INPUT id=__VIEWSTATE 
type=hidden value=/wEPDwUBMGRkkvnIJIfBq+X+PAH8jiEXU4uj+oE= name=__VIEWSTATE> 
</DIV>
<SCRIPT type=text/javascript>
//<![CDATA[
var theForm = document.forms['aspnetForm'];
if (!theForm) {
    theForm = document.aspnetForm;
}
function __doPostBack(eventTarget, eventArgument) {
    if (!theForm.onsubmit || (theForm.onsubmit() != false)) {
        theForm.__EVENTTARGET.value = eventTarget;
        theForm.__EVENTARGUMENT.value = eventArgument;
        theForm.submit();
    }
}
//]]>
</SCRIPT>

<SCRIPT src="Microsoft Sudoku：优化 UMPC 应用程序的点触和手写操作.files/WebResource.axd" 
type=text/javascript></SCRIPT>

<SCRIPT type=text/javascript>
//<![CDATA[
var CurUserId="AxUFAAAAAAC3CAAAyIkPb6xN3OIVR2+t760eXA!!";var ClientURL="http%3a%2f%2fmsdn.microsoft.com%2fzh-cn%2flibrary(d%3ddefault)%2faa480686(l%3dzh-cn%2cv%3dMSDN.10).aspx";var maxRating=5;var RatingObjId="ctl00_ib1_Flyout_Rating1";var tbCommentsId="ctl00_ib1_Flyout_tbComments";var PageId="aa480686";var pageTag="contentRatingaa480686MSDN.10zh-cn";var sendBtnId="ctl00_ib1_Flyout_btnSend";var curRating="4";var contentRated=false;var RatingTooltipText="请单击星形符号并提供反馈";var RatedTooltipText="您已经为此内容评级。感谢您的反馈。";var userIPAddress="124.72.11.73";var MtpsPageDirection="ltr";var RatingFlyoutID="ctl00_ib1_Flyout";var Rating_Device="default";//]]>
</SCRIPT>

<SCRIPT 
src="Microsoft Sudoku：优化 UMPC 应用程序的点触和手写操作.files/jquery-1.2.6-bn20091211.js" 
type=text/javascript></SCRIPT>

<SCRIPT 
src="Microsoft Sudoku：优化 UMPC 应用程序的点触和手写操作.files/Silverlight-bn20091211.js" 
type=text/javascript></SCRIPT>

<SCRIPT type=text/javascript>
//<![CDATA[
var wt_nvr_ru = "WT_NVR_RU";var wt_fpcdom = ".microsoft.com";var wt_domlist = "msdn.microsoft.com";var wt_pathlist = "";var wt_paramlist = "DCSext.mtps_devcenter";var wt_siteid = "MSDN";var gDomain="m.webtrends.com";
var gDcsId="dcsmgru7m99k7mqmgrhudo0k8_8c6m";
var gFpc="WT_FPC";
/*<![CDATA[*/
if(document.cookie.indexOf(gFpc+"=")==-1){document.write("<scr"+"ipt type='text/javascript' src='"+"http"+(window.location.protocol.indexOf('https:')==0?'s':'')+"://"+gDomain+"/"+gDcsId+"/wtid.js"+"'><\/scr"+"ipt>");}
/*]]>*/
var detectedLocale = 'zh-cn';
var wtsp="msdnlib_hp";
var gTrackEvents = 0;
var omni_guid='d2ad8792-1429-4438-a7cc-1a1f8c846469';//]]>
</SCRIPT>

<SCRIPT language=JavaScript 
src="Microsoft Sudoku：优化 UMPC 应用程序的点触和手写操作.files/omni_rsid_msdn-bn20091211.js"></SCRIPT>
<NOSCRIPT><A title="Web Analytics" href="http://www.omniture.com/"><IMG height=1 
alt="" src="" width=1 border=0></A></NOSCRIPT>
<SCRIPT src="Microsoft Sudoku：优化 UMPC 应用程序的点触和手写操作.files/compositejscript2.js" 
type=text/javascript></SCRIPT>

<SCRIPT type=text/javascript>
//<![CDATA[
Sys.WebForms.PageRequestManager._initialize('ctl00$ScriptManager1', document.getElementById('aspnetForm'));
Sys.WebForms.PageRequestManager.getInstance()._updateControls([], [], [], 90);
//]]>
</SCRIPT>
 
<DIV id=rheader>
<DIV class=miniRatings id=ctl00_ib1_Panel1>
<DIV class=miniRatings_left id=ctl00_ib1_miniRatings_left><A class=itPrint 
id=ctl00_ib1_hlPrint title=可打印版本 
onclick="javascript:SubmitRateAction('printerFriendlyPage');" 
href="http://msdn.microsoft.com/zh-cn/library/aa480686(printer).aspx"><IMG 
class=LibC_rtg_print id=ctl00_ib1_imgPrint 
style="BORDER-TOP-WIDTH: 0px; BORDER-LEFT-WIDTH: 0px; BORDER-BOTTOM-WIDTH: 0px; BORDER-RIGHT-WIDTH: 0px" 
src="Microsoft Sudoku：优化 UMPC 应用程序的点触和手写操作.files/clear.gif">&nbsp;可打印版本</A>&nbsp;&nbsp; 
<A class=itFav id=ctl00_ib1_hlFav title=添加到收藏夹 
onclick="javascript:SubmitRateAction('addToFavorites');" 
href="javascript:OnSave();"><IMG class=LibC_rtg_save id=ctl00_ib1_imgFav 
style="BORDER-TOP-WIDTH: 0px; BORDER-LEFT-WIDTH: 0px; BORDER-BOTTOM-WIDTH: 0px; BORDER-RIGHT-WIDTH: 0px" 
src="Microsoft Sudoku：优化 UMPC 应用程序的点触和手写操作.files/clear.gif">&nbsp;添加到收藏夹</A>&nbsp;&nbsp; 
<A class=itSend id=ctl00_ib1_hlSendCont title=发送此内容 
onclick="javascript:SubmitRateAction('sendThisPage');" 
href="mailto:?subject=Microsoft Sudokuï¼ä¼å UMPC åºç¨ç¨åºçç¹è§¦åæåæä½&amp;body=此 URL 指向您可能感兴趣的 MSDN 页面： http://msdn.microsoft.com/zh-cn/library/aa480686.aspx"><IMG 
class=LibC_rtg_email id=ctl00_ib1_imgSendCont 
style="BORDER-TOP-WIDTH: 0px; BORDER-LEFT-WIDTH: 0px; BORDER-BOTTOM-WIDTH: 0px; BORDER-RIGHT-WIDTH: 0px" 
src="Microsoft Sudoku：优化 UMPC 应用程序的点触和手写操作.files/clear.gif">&nbsp;提交</A>&nbsp;&nbsp; 
&nbsp;&nbsp; </DIV>
<DIV class=miniRatings_right id=ctl00_ib1_miniRatings_right>
<DIV class=ratingFlyoutStatic id=ctl00_ib1_Flyout style="POSITION: relative">
<DIV id=ctl00_ib1_Flyout_rtgContainer title=打开脚本编写以提交评估结果并发送反馈 
style="DISPLAY: block; TEXT-ALIGN: right">
<TABLE>
  <TBODY>
  <TR>
    <TD style="LINE-HEIGHT: 70%; TEXT-ALIGN: right"><SPAN class=tbFont 
      id=ctl00_ib1_Flyout_rateTitleLabel>请单击以进行评分并提供反馈</SPAN> </TD>
    <TD id=ctl00_ib1_Flyout_tdRtg style="WIDTH: 85px" 
    onclick="if($get('ctl00_ib1_Flyout_tbComments')!=null &amp;&amp; $get('ctl00_ib1_Flyout_tbComments').visible == 'true') $get('ctl00_ib1_Flyout_tbComments').focus();">
      <DIV id=ctl00_ib1_Flyout_Rating1 dir=ltr title=请单击星形符号并提供反馈 
      style="HEIGHT: 16px"><A id=ctl00_ib1_Flyout_Rating1_A title=请单击星形符号并提供反馈 
      style="TEXT-DECORATION: none" 
      href="http://msdn.microsoft.com/zh-cn/library/aa480686.aspx#"><SPAN 
      class="ratingStar filledRatingStar" id=ctl00_ib1_Flyout_Rating1_Star_1 
      style="FLOAT: left">&nbsp;</SPAN><SPAN class="ratingStar filledRatingStar" 
      id=ctl00_ib1_Flyout_Rating1_Star_2 style="FLOAT: left">&nbsp;</SPAN><SPAN 
      class="ratingStar filledRatingStar" id=ctl00_ib1_Flyout_Rating1_Star_3 
      style="FLOAT: left">&nbsp;</SPAN><SPAN class="ratingStar filledRatingStar" 
      id=ctl00_ib1_Flyout_Rating1_Star_4 style="FLOAT: left">&nbsp;</SPAN><SPAN 
      class="ratingStar emptyRatingStar" id=ctl00_ib1_Flyout_Rating1_Star_5 
      style="FLOAT: left">&nbsp;</SPAN></A> </DIV></TD></TR></TBODY></TABLE></DIV>
<DIV style="CLEAR: both; DISPLAY: block; FLOAT: none"></DIV>
<DIV class=ratingFlyoutPopup id=ctl00_ib1_Flyout_Popup 
style="LEFT: -1000px; VISIBILITY: hidden; POSITION: absolute"><SPAN 
class=OptionalText id=ctl00_ib1_Flyout_lbOptText>请就此内容提供反馈</SPAN><TEXTAREA class=Comment id=ctl00_ib1_Flyout_tbComments name=ctl00$ib1$Flyout$tbComments></TEXTAREA><INPUT class=Button id=ctl00_ib1_Flyout_btnClose onclick='CloseFLY("ctl00_ib1_Flyout") ' type=button value=关闭 name=ctl00$ib1$Flyout$btnClose><INPUT class=Button id=ctl00_ib1_Flyout_btnSend onclick="return IsPageRated(&quot;ctl00_ib1_Flyout_Rating1&quot;,&quot;ctl00_ib1_Flyout_lbOptText&quot;,'请先为本页内容评分', &quot;ctl00_ib1_Flyout&quot;,this)" type=button value=提交 name=ctl00$ib1$Flyout$btnSend></DIV>
<DIV class=TFlyPopupAnimate id=ctl00_ib1_Flyout_Anim></DIV></DIV></DIV>
<DIV id=intercleardiv 
style="CLEAR: both; DISPLAY: block; VISIBILITY: hidden"></DIV></DIV></DIV>
<DIV id=contents>
<DIV class=resizableArea id=ctl00_LibFrame>
<DIV class=leftSection id=ctl00_raLeft style="WIDTH: 179px"><IMG class=hidden 
id=ctl00_TocVector1_o 
style="BORDER-TOP-WIDTH: 0px; BORDER-LEFT-WIDTH: 0px; BORDER-BOTTOM-WIDTH: 0px; BORDER-RIGHT-WIDTH: 0px" 
src="Microsoft Sudoku：优化 UMPC 应用程序的点触和手写操作.files/clear.gif"> <IMG class=hidden 
id=ctl00_TocVector1_c 
style="BORDER-TOP-WIDTH: 0px; BORDER-LEFT-WIDTH: 0px; BORDER-BOTTOM-WIDTH: 0px; BORDER-RIGHT-WIDTH: 0px" 
src="Microsoft Sudoku：优化 UMPC 应用程序的点触和手写操作.files/clear.gif"> <IMG class=hidden 
id=ctl00_TocVector1_e 
style="BORDER-TOP-WIDTH: 0px; BORDER-LEFT-WIDTH: 0px; BORDER-BOTTOM-WIDTH: 0px; BORDER-RIGHT-WIDTH: 0px" 
src="Microsoft Sudoku：优化 UMPC 应用程序的点触和手写操作.files/clear.gif"> 
<DIV id=holder onclick=TocClick(event)>
<DIV class=rootToc id=ctl00_TocVector1_root>
<UL class=toc id=tocVectorTreeId>
  <LI name="ms310241(n)"><IMG class=LibC_c 
  src="Microsoft Sudoku：优化 UMPC 应用程序的点触和手写操作.files/clear.gif"><A 
  href="http://msdn.microsoft.com/zh-cn/library/ms123401.aspx" target=_top>MSDN 
  Library</A></LI></UL></DIV></DIV></DIV>
<DIV class=splitter id=ctl00_raSplitter title="单击并拖动可调整大小。双击或按 't' 可在隐藏和显示之间切换。" 
style="LEFT: 179px">
<DIV class=LibC_lib_grippy style="POSITION: absolute; TOP: 50%"></DIV></DIV>
<DIV class=rightSection id=ctl00_raRight>
<DIV>
<DIV class=EyebrowMenuBar id=ctl00_eb1_Panel1>
<DIV class=stat id=ctl00_eb1_ctl00 style="POSITION: relative"><A title=MSDN 
href="http://msdn.microsoft.com/zh-cn/ms348103.aspx">MSDN</A>
<DIV class=popup id=ctl00_eb1_ctl00_Popup 
style="LEFT: -1000px; VISIBILITY: hidden; POSITION: absolute"><SPAN 
name="http://msdn.microsoft.com/zh-cn/library/36cf08ac-18d1-4fe0-abc8-466852a0f9c7(n).aspx">请稍候</SPAN></DIV>
<DIV class=TFlyPopupAnimate id=ctl00_eb1_ctl00_Anim></DIV></DIV>
<DIV class=stat id=ctl00_eb1_ctl01 style="POSITION: relative"><A 
title="MSDN Library" 
href="http://msdn.microsoft.com/zh-cn/library/ms123401.aspx">MSDN Library</A>
<DIV class=popup id=ctl00_eb1_ctl01_Popup 
style="LEFT: -1000px; VISIBILITY: hidden; POSITION: absolute"><SPAN 
name="http://msdn.microsoft.com/zh-cn/library/ms310241(n).aspx">请稍候</SPAN></DIV>
<DIV class=TFlyPopupAnimate id=ctl00_eb1_ctl01_Anim></DIV></DIV>
<DIV class=stat id=ctl00_eb1_ctl02 style="POSITION: relative"><A title=技术文章 
href="http://msdn.microsoft.com/zh-cn/library/bb814937.aspx">技术文章</A>
<DIV class=popup id=ctl00_eb1_ctl02_Popup 
style="LEFT: -1000px; VISIBILITY: hidden; POSITION: absolute"><SPAN 
name="http://msdn.microsoft.com/zh-cn/library/bb814937(n).aspx">请稍候</SPAN></DIV>
<DIV class=TFlyPopupAnimate id=ctl00_eb1_ctl02_Anim></DIV></DIV>
<DIV class=stat id=ctl00_eb1_ctl03 style="POSITION: relative"><A title="MSDN 开发" 
href="http://msdn.microsoft.com/zh-cn/library/bb507200.aspx">MSDN 开发</A>
<DIV class=popup id=ctl00_eb1_ctl03_Popup 
style="LEFT: -1000px; VISIBILITY: hidden; POSITION: absolute"><SPAN 
name="http://msdn.microsoft.com/zh-cn/library/bb507200(n).aspx">请稍候</SPAN></DIV>
<DIV class=TFlyPopupAnimate id=ctl00_eb1_ctl03_Anim></DIV></DIV>
<DIV class=nochildstat><STRONG>&nbsp;Microsoft Sudoku：优化 UMPC 
应用程序的点触和手写...</STRONG></DIV><SPAN 
style="CLEAR: both; DISPLAY: block; FLOAT: none"></SPAN></DIV></DIV>
<DIV class=RightPanel>
<DIV class=ContentBar>
<STYLE type=text/css>.vb {
	DISPLAY: none
}
.cs {
	DISPLAY: none
}
.cpp {
	DISPLAY: none
}
.nu {
	DISPLAY: none
}
.fs {
	DISPLAY: none
}
</STYLE>

<STYLE id=LanguageSpecificTextStyle type=text/css>.nu {
	DISPLAY: inline
}
</STYLE>
</DIV>
<DIV class=ContentArea>
<DIV class=topic>
<DIV class=majorTitle xmlns:msxsl="urn:schemas-microsoft-com:xslt"></DIV>
<DIV class=title xmlns:msxsl="urn:schemas-microsoft-com:xslt">Microsoft 
Sudoku：优化 UMPC 应用程序的点触和手写操作 </DIV><!--Content type: DocStudio. Transform: psdk2mtps.xslt.-->
<DIV id=mainSection>
<DIV id=mainBody>
<DIV>
<DIV>发布日期 : 2006-7-5<SPAN> | </SPAN>更新日期 : 2006-7-5</DIV>
<P>适用于<BR>Windows XP Tablet PC Edition<BR>Microsoft .NET Framework</P>
<P><B>摘要：</B>Stephen Toub 将为您讲解填数游戏 Sudoku 的玩法，并展示如何编写应用程序来完成游戏、生成新游戏，以及如何增加在 
ultra-mobile PC 和 Tablet PC 上玩游戏的乐趣。</P>
<P>单击<A id=ctl00_MTCS_main_ctl01 
onclick="javascript:Track('ctl00_MTCS_main_ctl00|ctl00_MTCS_main_ctl01',this);" 
href="http://download.microsoft.com/download/0/6/7/0678a19e-59e9-407d-8326-bbea26b26fb5/SudokuSourceCodeSample.exe">此处</A>可下载本文引用的示例代码。</P>
<H5>本页内容</H5>
<P><A href="http://msdn.microsoft.com/zh-cn/library/aa480686.aspx#E0LC0AA"><IMG 
alt=简介 
src="Microsoft Sudoku：优化 UMPC 应用程序的点触和手写操作.files/Aa480686.arrow_px_down(zh-cn,MSDN.10).gif" 
border=0> </A><A 
href="http://msdn.microsoft.com/zh-cn/library/aa480686.aspx#E0LC0AA">简介</A> 
<BR><A href="http://msdn.microsoft.com/zh-cn/library/aa480686.aspx#E0IC0AA"><IMG 
alt="什么是 Sudoku？" 
src="Microsoft Sudoku：优化 UMPC 应用程序的点触和手写操作.files/Aa480686.arrow_px_down(zh-cn,MSDN.10).gif" 
border=0> </A><A 
href="http://msdn.microsoft.com/zh-cn/library/aa480686.aspx#E0IC0AA">什么是 
Sudoku？</A> <BR><A 
href="http://msdn.microsoft.com/zh-cn/library/aa480686.aspx#E0FC0AA"><IMG 
alt=应用程序基础架构 
src="Microsoft Sudoku：优化 UMPC 应用程序的点触和手写操作.files/Aa480686.arrow_px_down(zh-cn,MSDN.10).gif" 
border=0> </A><A 
href="http://msdn.microsoft.com/zh-cn/library/aa480686.aspx#E0FC0AA">应用程序基础架构</A> 
<BR><A href="http://msdn.microsoft.com/zh-cn/library/aa480686.aspx#E0CC0AA"><IMG 
alt="为 Ultra-Mobile PC 设计应用程序" 
src="Microsoft Sudoku：优化 UMPC 应用程序的点触和手写操作.files/Aa480686.arrow_px_down(zh-cn,MSDN.10).gif" 
border=0> </A><A 
href="http://msdn.microsoft.com/zh-cn/library/aa480686.aspx#E0CC0AA">为 
Ultra-Mobile PC 设计应用程序</A> <BR><A 
href="http://msdn.microsoft.com/zh-cn/library/aa480686.aspx#E06B0AA"><IMG 
alt=处理严重异常错误 
src="Microsoft Sudoku：优化 UMPC 应用程序的点触和手写操作.files/Aa480686.arrow_px_down(zh-cn,MSDN.10).gif" 
border=0> </A><A 
href="http://msdn.microsoft.com/zh-cn/library/aa480686.aspx#E06B0AA">处理严重异常错误</A> 
<BR><A href="http://msdn.microsoft.com/zh-cn/library/aa480686.aspx#E03B0AA"><IMG 
alt=单实例应用程序 
src="Microsoft Sudoku：优化 UMPC 应用程序的点触和手写操作.files/Aa480686.arrow_px_down(zh-cn,MSDN.10).gif" 
border=0> </A><A 
href="http://msdn.microsoft.com/zh-cn/library/aa480686.aspx#E03B0AA">单实例应用程序</A> 
<BR><A href="http://msdn.microsoft.com/zh-cn/library/aa480686.aspx#E0ZB0AA"><IMG 
alt="Sudoku 算法" 
src="Microsoft Sudoku：优化 UMPC 应用程序的点触和手写操作.files/Aa480686.arrow_px_down(zh-cn,MSDN.10).gif" 
border=0> </A><A 
href="http://msdn.microsoft.com/zh-cn/library/aa480686.aspx#E0ZB0AA">Sudoku 
算法</A> <BR><A 
href="http://msdn.microsoft.com/zh-cn/library/aa480686.aspx#E0WB0AA"><IMG 
alt=分析和完成填数游戏 
src="Microsoft Sudoku：优化 UMPC 应用程序的点触和手写操作.files/Aa480686.arrow_px_down(zh-cn,MSDN.10).gif" 
border=0> </A><A 
href="http://msdn.microsoft.com/zh-cn/library/aa480686.aspx#E0WB0AA">分析和完成填数游戏</A> 
<BR><A href="http://msdn.microsoft.com/zh-cn/library/aa480686.aspx#E0TB0AA"><IMG 
alt=生成游戏 
src="Microsoft Sudoku：优化 UMPC 应用程序的点触和手写操作.files/Aa480686.arrow_px_down(zh-cn,MSDN.10).gif" 
border=0> </A><A 
href="http://msdn.microsoft.com/zh-cn/library/aa480686.aspx#E0TB0AA">生成游戏</A> 
<BR><A href="http://msdn.microsoft.com/zh-cn/library/aa480686.aspx#E0QB0AA"><IMG 
alt="Windows 窗体和硬件交互" 
src="Microsoft Sudoku：优化 UMPC 应用程序的点触和手写操作.files/Aa480686.arrow_px_down(zh-cn,MSDN.10).gif" 
border=0> </A><A 
href="http://msdn.microsoft.com/zh-cn/library/aa480686.aspx#E0QB0AA">Windows 
窗体和硬件交互</A> <BR><A 
href="http://msdn.microsoft.com/zh-cn/library/aa480686.aspx#E0NB0AA"><IMG 
alt="绘制 Sudoku" 
src="Microsoft Sudoku：优化 UMPC 应用程序的点触和手写操作.files/Aa480686.arrow_px_down(zh-cn,MSDN.10).gif" 
border=0> </A><A 
href="http://msdn.microsoft.com/zh-cn/library/aa480686.aspx#E0NB0AA">绘制 
Sudoku</A> <BR><A 
href="http://msdn.microsoft.com/zh-cn/library/aa480686.aspx#E0KB0AA"><IMG 
alt="启用 Tablet PC 交互" 
src="Microsoft Sudoku：优化 UMPC 应用程序的点触和手写操作.files/Aa480686.arrow_px_down(zh-cn,MSDN.10).gif" 
border=0> </A><A 
href="http://msdn.microsoft.com/zh-cn/library/aa480686.aspx#E0KB0AA">启用 Tablet 
PC 交互</A> <BR><A 
href="http://msdn.microsoft.com/zh-cn/library/aa480686.aspx#E0HB0AA"><IMG 
alt=识别多个笔画组成的数字 
src="Microsoft Sudoku：优化 UMPC 应用程序的点触和手写操作.files/Aa480686.arrow_px_down(zh-cn,MSDN.10).gif" 
border=0> </A><A 
href="http://msdn.microsoft.com/zh-cn/library/aa480686.aspx#E0HB0AA">识别多个笔画组成的数字</A> 
<BR><A href="http://msdn.microsoft.com/zh-cn/library/aa480686.aspx#E0EB0AA"><IMG 
alt=撤消支持 
src="Microsoft Sudoku：优化 UMPC 应用程序的点触和手写操作.files/Aa480686.arrow_px_down(zh-cn,MSDN.10).gif" 
border=0> </A><A 
href="http://msdn.microsoft.com/zh-cn/library/aa480686.aspx#E0EB0AA">撤消支持</A> 
<BR><A href="http://msdn.microsoft.com/zh-cn/library/aa480686.aspx#E0BB0AA"><IMG 
alt=便签簿支持 
src="Microsoft Sudoku：优化 UMPC 应用程序的点触和手写操作.files/Aa480686.arrow_px_down(zh-cn,MSDN.10).gif" 
border=0> </A><A 
href="http://msdn.microsoft.com/zh-cn/library/aa480686.aspx#E0BB0AA">便签簿支持</A> 
<BR><A href="http://msdn.microsoft.com/zh-cn/library/aa480686.aspx#E5AA"><IMG 
alt=橡皮擦支持 
src="Microsoft Sudoku：优化 UMPC 应用程序的点触和手写操作.files/Aa480686.arrow_px_down(zh-cn,MSDN.10).gif" 
border=0> </A><A 
href="http://msdn.microsoft.com/zh-cn/library/aa480686.aspx#E5AA">橡皮擦支持</A> 
<BR><A href="http://msdn.microsoft.com/zh-cn/library/aa480686.aspx#E2AA"><IMG 
alt=胜利动画 
src="Microsoft Sudoku：优化 UMPC 应用程序的点触和手写操作.files/Aa480686.arrow_px_down(zh-cn,MSDN.10).gif" 
border=0> </A><A 
href="http://msdn.microsoft.com/zh-cn/library/aa480686.aspx#E2AA">胜利动画</A> 
<BR><A href="http://msdn.microsoft.com/zh-cn/library/aa480686.aspx#EYAA"><IMG 
alt=保存状态和设置 
src="Microsoft Sudoku：优化 UMPC 应用程序的点触和手写操作.files/Aa480686.arrow_px_down(zh-cn,MSDN.10).gif" 
border=0> </A><A 
href="http://msdn.microsoft.com/zh-cn/library/aa480686.aspx#EYAA">保存状态和设置</A> 
<BR><A href="http://msdn.microsoft.com/zh-cn/library/aa480686.aspx#EVAA"><IMG 
alt=电源问题 
src="Microsoft Sudoku：优化 UMPC 应用程序的点触和手写操作.files/Aa480686.arrow_px_down(zh-cn,MSDN.10).gif" 
border=0> </A><A 
href="http://msdn.microsoft.com/zh-cn/library/aa480686.aspx#EVAA">电源问题</A> 
<BR><A href="http://msdn.microsoft.com/zh-cn/library/aa480686.aspx#ESAA"><IMG 
alt="扩展 Sudoku " 
src="Microsoft Sudoku：优化 UMPC 应用程序的点触和手写操作.files/Aa480686.arrow_px_down(zh-cn,MSDN.10).gif" 
border=0> </A><A 
href="http://msdn.microsoft.com/zh-cn/library/aa480686.aspx#ESAA">扩展 Sudoku 
</A><BR><A 
href="http://msdn.microsoft.com/zh-cn/library/aa480686.aspx#EPAA"><IMG alt=打印游戏 
src="Microsoft Sudoku：优化 UMPC 应用程序的点触和手写操作.files/Aa480686.arrow_px_down(zh-cn,MSDN.10).gif" 
border=0> </A><A 
href="http://msdn.microsoft.com/zh-cn/library/aa480686.aspx#EPAA">打印游戏</A> 
<BR><A href="http://msdn.microsoft.com/zh-cn/library/aa480686.aspx#EMAA"><IMG 
alt=支持多处理器 
src="Microsoft Sudoku：优化 UMPC 应用程序的点触和手写操作.files/Aa480686.arrow_px_down(zh-cn,MSDN.10).gif" 
border=0> </A><A 
href="http://msdn.microsoft.com/zh-cn/library/aa480686.aspx#EMAA">支持多处理器</A> 
<BR><A href="http://msdn.microsoft.com/zh-cn/library/aa480686.aspx#EJAA"><IMG 
alt=将填数游戏复制到剪贴板 
src="Microsoft Sudoku：优化 UMPC 应用程序的点触和手写操作.files/Aa480686.arrow_px_down(zh-cn,MSDN.10).gif" 
border=0> </A><A 
href="http://msdn.microsoft.com/zh-cn/library/aa480686.aspx#EJAA">将填数游戏复制到剪贴板</A> 
<BR><A href="http://msdn.microsoft.com/zh-cn/library/aa480686.aspx#EGAA"><IMG 
alt=其他操作... 
src="Microsoft Sudoku：优化 UMPC 应用程序的点触和手写操作.files/Aa480686.arrow_px_down(zh-cn,MSDN.10).gif" 
border=0> </A><A 
href="http://msdn.microsoft.com/zh-cn/library/aa480686.aspx#EGAA">其他操作...</A> 
<BR><A href="http://msdn.microsoft.com/zh-cn/library/aa480686.aspx#EDAA"><IMG 
alt=致谢 
src="Microsoft Sudoku：优化 UMPC 应用程序的点触和手写操作.files/Aa480686.arrow_px_down(zh-cn,MSDN.10).gif" 
border=0> </A><A 
href="http://msdn.microsoft.com/zh-cn/library/aa480686.aspx#EDAA">致谢</A> <BR><A 
href="http://msdn.microsoft.com/zh-cn/library/aa480686.aspx#EAAA"><IMG alt=作者简介 
src="Microsoft Sudoku：优化 UMPC 应用程序的点触和手写操作.files/Aa480686.arrow_px_down(zh-cn,MSDN.10).gif" 
border=0> </A><A 
href="http://msdn.microsoft.com/zh-cn/library/aa480686.aspx#EAAA">作者简介</A> 
<BR></P>
<H3 id=E0LC0AA>简介</H3>
<P>去年，在纽约市西村的一家小咖啡馆，有人向我介绍了一款铅笔填数游戏，我的朋友 Luke 
完全被这款游戏迷住了。当时与其他人的愉快谈话使我免于步其后尘，可是却没想到，躲得过初一躲不过十五。几个月之后，也就是 2005 年 6 
月，我去伦敦看望兄弟和大学室友时又碰到了 Sudoku 游戏。显见 Sudoku 
已经风靡一时，无弗远近。我从好奇、试探，最后也沉迷于其中。对开发人员来说往往会是这样，今天的激情极有可能转化为明天的开发项目。这次也毫不例外，我飞回美国后很快安装了第一个版本的 
C# Sudoku 游戏，并马上开始运行。</P>
<P>而我对它却不太满意，总感觉缺点什么。用笔在纸上计算并得出正确数字是一件让人心情愉悦的事。而问题在于游戏转化为 Windows 
表格形式后，这种乐趣不见了。这个问题深深地困扰着我。Sudoku 是一种非常适合在 Tablet PC 上实现的应用程序。第二天我下载了 Tablet PC 
SDK，很快就将这款由键盘和鼠标操作的游戏改为笔控操作。（请注意示例程序文件中的程序员注释使用的是英文，本文中将其译为中文是为了便于参考。本文章还包含指向英文网页的链接。）</P>
<P><IMG alt="" 
src="Microsoft Sudoku：优化 UMPC 应用程序的点触和手写操作.files/Aa480686.tbconsudokusamplefigure01(zh-cn,MSDN.10).gif"> 
</P>
<DIV><B>图 1：Microsoft Sudoku</B> </DIV>
<P>本文就如何基于 Tablet PC 实现 Sudoku 进行了深入探讨。与原来在 Touch Pack 上的实现方法相同，ultra-mobile PC 
(UMPC) 上预装了软件包。除了 Sudoku 游戏的特定细节外，本文还详述了游戏实现的算法问题，有助于您在 Tablet PC 上实现其他的应用程序设计。 
</P>
<DIV><A 
href="http://msdn.microsoft.com/zh-cn/library/aa480686.aspx#mainSection"><IMG 
alt="" 
src="Microsoft Sudoku：优化 UMPC 应用程序的点触和手写操作.files/Aa480686.arrow_px_up(zh-cn,MSDN.10).gif" 
border=0> </A><A 
href="http://msdn.microsoft.com/zh-cn/library/aa480686.aspx#mainSection">返回页首</A> 
<BR></DIV>
<H3 id=E0IC0AA>什么是 Sudoku？</H3>
<P>Sudoku 是一种填数游戏，风靡于日本和欧洲。在很短的时间内它又迅速占领了美国市场 － 书店里摆满了 Sudoku 
产品，报纸也连篇累牍地对其进行报道。游戏的原型是一个 9x9 的网格，分成九个 3x3 的小框，每一小框包含 9 个单元格。如图 2 所示为包含 81 
个单元格的完整图形。</P>
<P><IMG alt="" 
src="Microsoft Sudoku：优化 UMPC 应用程序的点触和手写操作.files/Aa480686.tbconsudokusamplefigure02(zh-cn,MSDN.10).gif"> 
</P>
<DIV><B>图 2：空的 Sudoku 网格</B> </DIV>
<P>这款游戏的玩法是：在每个单元格中填入一个数字（1-9），最后每行、每列和每一小框的九个单元格中都包含有全部的 9 个数字（超级 Sudoku 
难度更高，它使用 16x16 网格，填入数字 1-9 和字母 A-G）。 </P>
<P>如图 3 显示为一个 Sudoku 有效解法。</P>
<P><IMG alt=tbconsudokusamplefigure03 
src="Microsoft Sudoku：优化 UMPC 应用程序的点触和手写操作.files/Aa480686.tbconsudokusamplefigure03(zh-cn,MSDN.10).gif"> 
</P>
<DIV><B>图 3：Sudoku 有效解法</B> </DIV>
<P>通过计算，9x9 Sudoku 共有 10<SUP>21</SUP> 种以上的有效解法（请登录 <A id=ctl00_MTCS_main_ctl02 
onclick="javascript:Track('ctl00_MTCS_main_ctl00|ctl00_MTCS_main_ctl02',this);" 
href="http://go.microsoft.com/fwlink/?LinkId=63523">http.//www.afjarvis.staff.shef.ac.uk/sudoku/</A> 
了解有关这方面计算的详细信息）。开局时，网格中已经填了一些数字，而其魅力就在于，每局游戏只有一种正确解法。您必须通过逻辑推导填入所有缺失的数字。</P>
<P>以图 4 所示的游戏为例（为便于讲解，我们以黄色和红色突出显示了几个单元格）。如上所述，每一小框都要包含全部的九个数字。现在，左上角的框中没有 
2，只需简单推导一下就能知道 2 应该填入哪个单元格。第一行不行，因为第一行已经有 2 了（在右上角，已经用黄色突出显示）。中间一行也不行，这一行也有 2 
（在第二行、第四列）。很明显也不能是第三行第二列，该单元格已经有一个 7 （以黄色突出显示）；也不可能是 7 右侧的单元格，这一列也有 
2（第八行，以黄色突出显示）。所以，2 一定是填在第三行第一列，即以红色突出显示的单元格。确定了该框内 2 的位置后，我们离完成游戏更近了一步。</P>
<P><IMG alt="" 
src="Microsoft Sudoku：优化 UMPC 应用程序的点触和手写操作.files/Aa480686.tbconsudokusamplefigure04(zh-cn,MSDN.10).gif"> 
</P>
<DIV><B>图 4：开始 Sudoku 游戏</B> </DIV>
<P>现在您已经对游戏有了一定了解，本文的其余内容主要是关于如何在 Tablet PC 上实现 Sudoku 游戏的。首先，让我们来看一下 Tablet PC 
应用程序的基本要求，并说明如何在 Sudoku 中满足这些要求，以及您在编写自己的应用程序时应该怎么做。接下来，我会具体讲解 Sudoku 
游戏实现的具体问题，主要是如何通过适当算法完成 Sudoku 的相关任务，如生成新游戏和找出游戏解法。然后再讲一下有关 Tablet PC 
的具体细节问题，主要是如何使用 Tablet PC API 来支持笔式交互。最后，我将会给出一些有趣的概念和示例，您可以用自己喜欢的方法玩游戏。</P>
<DIV><A 
href="http://msdn.microsoft.com/zh-cn/library/aa480686.aspx#mainSection"><IMG 
alt="" 
src="Microsoft Sudoku：优化 UMPC 应用程序的点触和手写操作.files/Aa480686.arrow_px_up(zh-cn,MSDN.10).gif" 
border=0> </A><A 
href="http://msdn.microsoft.com/zh-cn/library/aa480686.aspx#mainSection">返回页首</A> 
<BR></DIV>
<H3 id=E0FC0AA>应用程序基础架构</H3>
<P>本部分涉及的概念适用于一般的 Tablet PC 应用程序，并非仅限于 Sudoku 游戏。</P>
<P>平台检测</P>
<P>多数为 Tablet PC 编写的程序只有在 Tablet PC 或采用相应配置的开发计算机上才能达到最佳运行状态。开发计算机需要同时安装 
Microsoft Windows XP Tablet PC Edition Software Development Kit 1.7 和 Microsoft 
Windows XP Tablet PC Edition 2005 Recognizer Pack（可从 <A id=ctl00_MTCS_main_ctl03 
onclick="javascript:Track('ctl00_MTCS_main_ctl00|ctl00_MTCS_main_ctl03',this);" 
href="http://msdn.microsoft.com/mobilepc">Mobile PC Developer 
Center</A>（英文）获得这两个软件）。造成这种现象的部分原因是对 Microsoft.Ink.dll 程序集的依赖，该程序集与 Windows XP 
Tablet PC Edition 2005 一同发布。因而，在尝试执行依赖于这些程序集存在的代码之前，有必要检查 Tablet PC 
应用程序是否可以使用必需的库和功能。由于必须使用平台检测来执行存在性检查，因此，即使应用程序定义为仅在发现库的时候才对库进行加载和使用，亦需执行如此操作， 
</P>
<P><B>注意</B> <B></B>重新发布 tpcman17.msm 和 mstpcrt.msmneed 时并不需要检查 Tablet PC 
二进制文件，但您需要保证只在运行 Windows XP Tablet PC Edition 2005 的计算机上部署应用程序。如果您无法重新发布 Tablet 
PC 二进制文件，并且是在没有 Tablet PC 
二进制的计算机上部署应用程序，则需要执行这些检查以确保应用程序的正常运行。在某些情形下需要特别注意这一点，例如在应用程序包含手写控件且部署于网站时。</P>
<P>在随附的示例代码中，所有平台检测代码均包含在 PlatformDetection.cs 文件的 <B>PlatformDetection</B> 
类中，该文件位于 Utilities 文件夹下。该类包含的代码可用来执行多个不同的检查，还可以将查询结果提供给应用程序的其余部分。</P>
<P>首先，在启动后不久，您的应用程序会检查是否有可用的 Microsoft.Ink.dll 程序集。首先编译来自 Microsoft.Ink.dll 
程序集的代码引用类时，将隐式执行这项检查，但是您并不会希望这样，因为在您没有预计会发生异常时，可能会很难从 JIT 
编译器引发的异常中进行正确恢复。另外一种方法是，您可以选择使用 <B>Assembly.Load</B> 和 
<B>Assembly.LoadWithPartialName</B> 方法来进行显式搜索，以确定是否能找到有效的 Microsoft.Ink.dll 
程序集。</P><PRE>public static bool InkAssemblyAvailable 
{
get { return _inkAssemblyAvailable; } 
}

private static bool _inkAssemblyAvailable = 
(LoadInkAssembly() != null);

private static Assembly LoadInkAssembly()
{
try 
  {
Assembly a = Assembly.Load(
"Microsoft.Ink, Version=1.7.2600.2180, " + 
"Culture=neutral, PublicKeyToken=31bf3856ad364e35");
if (a == null)
    {
a = Assembly.LoadWithPartialName(
"Microsoft.Ink, PublicKeyToken=31bf3856ad364e35");
if (a != null &amp;&amp; a.GetName().Version &lt; 
new Version("1.7.2600.2180")) a = null;
    }
return a;
  }
catch(IOException){}
catch(SecurityException){}
catch(BadImageFormatException){}
return null;
}
</PRE>
<P>公共的静态 <B>InkAssemblyAvailable</B> 属性返回的值存储在一个私有静态布尔值中，该布尔值以调用 
<B>LoadInkAssembly</B> 的返回值进行初始化。<B>LoadInkAssembly</B> 
使用程序集的全名（包括版本号、区域性和公钥标记），通过反射 API 来加载 Microsoft.Ink 程序集。如果加载程序无法找到包含这些信息的程序集，则 
<B>LoadInkAssembly</B> 会尝试使用部分名称来加载 Microsoft.Ink 
程序集。也就是说，执行搜索时会省略一些程序集全名信息。在此示例中，我省略了版本号。这种思路是，Sudoku 实现程序可能会与更新版本的 
Microsoft.Ink.dll 一起使用（例如，如果游戏在 Windows Vista 下运行该版本的 Tablet PC 
API），则通过省略版本号，我允许加载程序查找所有版本的程序集。当然，假设找到的每一版本都能符合要求未免有些不切合实际；游戏有可能在缺少某些功能支持的较旧版本上运行，而如果在此时 
JIT 编译器尝试对缺失部分的使用进行编译的话，将会导致操作失败。为避免这种情况的发生，我做了一个更为安全的假设（也不是 100% 
的安全），即只接受较新的版本。因此，在按部分名称加载程序集之后，我检查其版本号以确保是经过我编译的版本或较新版本。</P>
<P>经过验证还有一种更好的选择（我也实际使用过），就是让 CLR 以其标准逻辑来尝试找到并加载适当的 Microsoft.Ink.dll 
程序集，然后对程序集不可用或无法加载的情况进行处理。我在前面已经提到，在您没有预计会发生异常时，通常很难从程序集和类型加载异常中恢复。但是当您意识到可能会发生异常，并在受控环境下充分利用它们时，凭借 
CLR 的功能通常是最好的方法。我可以按照下列方法重新编写 <B>LoadInkAssembly</B> 来更改我的实现程序。</P><PRE>private static Assembly LoadInkAssembly()
{
try { return LoadInkAssemblyInternal(); }
catch(TypeLoadException) {}
catch(IOException){}
catch(SecurityException){}
catch(BadImageFormatException){}
return null;
}
[MethodImpl(MethodImplOptions.NoInlining)]
private static Assembly LoadInkAssemblyInternal()
{
return typeof(InkOverlay).Assembly;
}
</PRE>
<P>回到程序的 <B>Main</B> 方法（位于 Program.cs 文件中），我使用 <B>InkAssemblyAvailable</B> 
属性来确定是否应该继续执行。<B>Main</B> 之后会显示一个消息框，如不允许继续执行则会退出。</P><PRE>bool validSystem = PlatformDetection.InkAssemblyAvailable &amp;&amp; 
PlatformDetection.RecognizerInstalled;
if (!validSystem)
{
MessageBox.Show(ResourceHelper.NotRunningOnTablet,
ResourceHelper.DisplayTitle, MessageBoxButtons.OK,  
MessageBoxIcon.Error, MessageBoxDefaultButton.Button1);
return 1;
}
</PRE>
<P>请注意，我不仅要求了手写程序集，还要求安装识别程序。在典型的 Tablet PC 
安装中，手写程序集和识别程序总是成对出现。但是，也有可能出现识别程序被删除的情况。在开发环境下也有可能在没有识别程序的情况下安装了 Tablet PC 
API。因此，我使用下列代码来检查具体的情况。</P><PRE>public static bool RecognizerInstalled
{
get
  {
if (!InkAssemblyAvailable) return false;
return GetDefaultRecognizer() != null;
  }
}
</PRE>
<P><B>RecognizerInstalled</B> 属性首先检查手写程序集是否可用。这一点非常重要，因为 
<B>RecognizerInstalled</B> 属性用来查看识别程序是否可用的 <B>GetDefaultRecognizer</B> 
实现程序，依赖于来自 Microsoft.Ink.dll 的类型。</P><PRE>[MethodImpl(MethodImplOptions.NoInlining)]
public static Recognizer GetDefaultRecognizer()  
{
Recognizer recognizer = null;
try 
  { 
Recognizers recognizers = new Recognizers();
if (recognizers.Count &gt; 1)
    {
try { recognizer = recognizers.GetDefaultRecognizer(); }  
catch {}

if (recognizer == null)
      {
try 
        { 
recognizer =  
recognizers.GetDefaultRecognizer(1033);  
        }
catch {}
      }
    }
  }
catch {} 
return recognizer;
}
</PRE>
<P><B>GetDefaultRecognizer</B> 实例化 
Microsoft.Ink.Recognizers（英文）集合，并确保其中至少有一个识别程序可用。然后它会使用该集合来检索默认的识别程序。如果出于某种原因而无法如此操作，将会尝试检索 
LCID 1033（美国英语环境）的默认识别程序。（因为唯一由 Sudoku 执行的识别是针对整数的，这就已经足够了。） </P>
<P>请注意，我在 <B>GetDefaultRecognizer</B> 上使用了 
System.Runtime.CompilerServices.MethodImplAttribute（英文），将其标记为保持原样而不进行内联。<I>内联</I>是指编译器选择将目标函数内容复制到一个调用场所，而非真正进行方法调用的过程。对于较小的方法，执行方法的成本主要取决于进行方法调用的开销，而内联则可以大幅提升性能。实际上，因为 
<B>GetDefaultRecognizer</B> 方法中代码数量的问题，JIT 编译器几乎没有什么机会来执行内联过程。 </P>
<P>不管怎样，我都不希望 <B>GetDefaultRecognizer</B> 进行内联。请记住，当 JIT 编译一个方法时，JIT 
编译器会尝试加载所有包含方法所用类型的程序集。举例来说，当 JIT 编译<B> GetDefaultRecognizer</B> 时，JIT 编译器将会尝试加载 
Microsoft.Ink.dll。如果 <B>GetDefaultRecognizer</B> 已经内联至 
<B>RecognizerInstalled</B>，则 JIT 编译<B> RecognizerInstalled</B> 时，会在 
<B>RecognizerInstalled</B> 有机会检查是否存在手写程序集之前进行加载尝试。因此，我在 
<B>GetDefaultRecognizer</B> 上使用 <B>MethodImplAttribute</B> 来确保它不会进行内联。这使得 
<B>RecognizerInstalled</B> 首先执行程序集的存在性检查。</P>
<P>该 Sudoku 
实现程序支持使用擦除手势来清除玩家之前输入的数字。在大多数示例代码中，如果没有安装有效的手势识别程序，尝试支持手势功能将会导致异常现象的发生，但很遗憾的是，这些您根本看不到。因此，我还使用了 
<B>PlatformDetection</B> 来检查是否安装有手势识别程序。</P><PRE>public static bool GestureRecognizerInstalled
{
get
  {
return InkAssemblyAvailable &amp;&amp; 
GestureRecognizerInstalledInternal;
  }
}
private static bool GestureRecognizerInstalledInternal  
{
[MethodImpl(MethodImplOptions.NoInlining)]
get
  {
try
    {
Recognizers recognizers = new Recognizers();
if (recognizers.Count &gt; 0)
      {
using(new GestureRecognizer()) return true;
      }
    }
catch {} 
return false;
  }
}
</PRE>
<P>如 <B>RecognizerInstalled</B> 一样，<B>GestureRecognizerInstalled</B> 
公共静态属性不会显式地执行这项检查。由于其对 Microsoft.Ink 程序集的依赖关系，它同样会依赖于标记为 <B>NoInlining</B> 
的帮助者成员。<B>GestureRecognizerInstalledInternal</B> 属性检查是否存在识别程序。然后它显式实例化 
Microsoft.StylusInput.GestureRecognizer（英文），返回是否能够成功创建一个 
Microsoft.StylusInput.GestureRecognizer 的值。如果无法成功实例化，则会引发 
<B>InvalidOperationException</B>（因此，try-catch 围住实例化部分），表明“The requested 
recognizer is not available with the current setup or 
configuration”（请求的识别程序在当前设置或配置下不可用）。</P>
<P><B>PlatformDetection</B> 提供的最后一项功能，是确定用户的左右手使用习惯。Tablet PC 
操作系统允许用户根据左手操作或右手操作来配置系统。应用程序可查询此设置并修改用户界面，以便最好地适应用户的左右手使用习惯。</P><PRE>public static bool UserIsRightHanded
{
get
  {
if (IsRunningOnTablet)
    {
bool rightHanded = true;
if (NativeMethods.SystemParametersInfo(
NativeMethods.SPI_GETMENUDROPALIGNMENT, 0,  
ref rightHanded, 0))
      {
return rightHanded;
      }
    }
return true;
  }
}
private static bool IsRunningOnTablet
{ 
get 
  {
return NativeMethods.GetSystemMetrics(
NativeMethods.SM_TABLETPC) != 0;  
  }
}
</PRE>
<P>此属性首先检查当前系统是否为 Tablet PC。这里没有使用 
<B>InkAssemblyAvailable</B>，因为手写程序集的存在并不代表系统就是 Tablet PC，因而也不代表此设置可供查询。相反，其依赖于 
user32.dll 提供的 GetSystemMetrics 函数。</P><PRE>[DllImport("user32.dll", CharSet=CharSet.Auto)]
internal static extern int GetSystemMetrics(int nIndex);
</PRE>
<P>提供了 <B>SM_TABLETPC</B> 值 (86) 之后，如果当前操作系统为 Microsoft Windows XP Tablet PC 
Edition，则此函数返回一个非零值，否则返回一个零值。在知道应用程序将在 Tablet PC 上运行后，我可以使用 user32.dll 提供的 
SystemParametersInfo 函数来查询左右手使用习惯设置。用户首选项可通过 <B>SPI_GETMENUDROPALIGNMENT</B> 
设置得以体现，对于相应的菜单栏项目，该设置确定是以左对齐还是右对齐的方式弹出菜单。</P>
<DIV><A 
href="http://msdn.microsoft.com/zh-cn/library/aa480686.aspx#mainSection"><IMG 
alt="" 
src="Microsoft Sudoku：优化 UMPC 应用程序的点触和手写操作.files/Aa480686.arrow_px_up(zh-cn,MSDN.10).gif" 
border=0> </A><A 
href="http://msdn.microsoft.com/zh-cn/library/aa480686.aspx#mainSection">返回页首</A> 
<BR></DIV>
<H3 id=E0CC0AA>为 Ultra-Mobile PC 设计应用程序</H3>
<P>最近 Microsoft 和许多 OEM 推出了 ultra-mobile PC (UMPC) 这一体积小巧的计算机，对于我们这些熟悉 Tablet PC 
开发的人来说是一个绝好的机会。由于在 UMPC 上运行 Windows XP Tablet PC Edition，所以我们可以充分利用在 Tablet PC 
方面的开发经验。这就意味着每一 UMPC 中都含有 API，必然需要开发成熟的手写输入应用程序。实际上，您为 Tablet PC 
编写的应用程序应该可以毫无障碍地在 UMPC 上运行。但是，在为 UMPC 设备编写新软件或调整现有软件以期更佳的 UMPC 体验时，需要特别注意如下几点。 
</P>
<P>首先是 UMPC 的屏幕较小，通常为 800x480。这就需要您清楚了解应用程序要求什么类型的屏幕。此外，如果您的应用程序使用对象除了 UMPC 
之外还有标准的 Tablet PC，则需要对应用程序进行精心设计，使之在不同的屏幕尺寸上都能达到最佳效果。我将在 Windows 窗体和硬件交互中讨论如何在 
Sudoku 中处理这种情况。</P>
<P>今天大多数 Tablet PC 拥有的是电磁感应屏幕，可与专用手写笔进行复杂的检测与交互，而 UMPC 
配备的是触敏式屏幕。这样的话设备使用起来就更为简便，用户用手指或其他指点设备就可以轻松地操作 
UMPC。但是，由于大多数的触摸屏上缺少手掌误触防护，用户体验起来与配备电磁感应数字板的 Tablet PC 
会有不同。所以在设计应用程序用户界面时一定要考虑到这一点。我的 Sudoku 
支持多种交互方式。使用键盘或手写笔可以轻松地在游戏中输入数字，而且还提供了较大的数字按钮，这样操作起来就更为简单，只需用手指点击一个数字，然后轻触在 
Sudoku 网格内的单元格，单元格中就会出现该数字。</P>
<P>另外一个需要密切注意的地方是 UMPC 中的手写笔只是一个简单的指点设备，而不是电磁感应设备。也就是说其应用程序不能像配备电磁感应数字板的 Tablet 
PC 那样，通过手写笔的交互接收到同样多的信息。因此为保证各项功能的正常运行，请确保专为 UMPC 
设计的应用程序不要依赖于其他信息，如空中数据包或数字版压力。</P>
<P>最重要的一点是，您完全可以借鉴在 Tablet PC 方面的开发知识。您在设计和开发 UMPC 
应用程序时如果能够考虑到这些方面，就可以为用户带来更好的体验。正如所见，我开发的 Sudoku 在标准 Tablet PC 和 UMPC 
设备上都能正常运行。</P>
<P>代码访问安全性</P>
<P>乍看之下，可能会有人认为像 Sudoku 
这样的游戏实现无需考虑什么安全问题，但实情却并非如此。作为一名开发人员，应该始终关注安全问题，关注应该为客户带来什么样的安全相关要求。</P>
<P>例如 Sudoku 中要用到几个 Win32 API。访问这些 API 要求有非托管代码权限。如果 Sudoku 是从本地驱动器运行、默认的代码访问安全性 
(CAS) 设置被完整保留，那么就万无一失了。但是，如果有人试图以网络共享的方式运行 Sudoku 将会如何？默认情况下，不会向 Intranet 
应用程序授予非托管代码权限。因此，当 Sudoku 试图访问这些非托管 API 时将会引发系统的安全异常。</P>
<P>与其在部分信任的环境下运行 Sudoku，还不如像大多数托管应用程序一样，让 Sudoku 在完全信任的环境下运行。而且，如果在部分信任的环境下运行 
Sudoku，那么用户体验也会极其糟糕，应用程序将在玩家面前发生悲剧性的灾难。所以，Sudoku 
在启动之后会明确查看是否为完全信任的环境，如果与当前环境不兼容则向用户发出警告。以下是启动 Sudoku <B>主</B>方法的代码。</P><PRE>[STAThread]
public static int Main()
{
Application.EnableVisualStyles();
Application.DoEvents();
if (!HasFullTrust)
  {
MessageBox.Show(ResourceHelper.LacksMinimumPermissionsToRun,  
ResourceHelper.DisplayTitle, MessageBoxButtons.OK,  
MessageBoxIcon.Error, MessageBoxDefaultButton.Button1);
return 1;
}
</PRE>
<P>Sudoku 会检查是否已经授予应用程序完全信任级别。如果没有，Sudoku 
会向用户显示一个友好的错误对话框，然后退出。<B>HasFullTrust</B> 属性可通过如下代码实现。</P><PRE>private static bool HasFullTrust
{
get
  {
try
    {
new PermissionSet(PermissionState.Unrestricted).Demand();
return true;
    }
catch (SecurityException) { return false; }
  }
}
</PRE>
<P>该属性将 System.Security.PermissionSet 实例化为不受限制、完全信任的访问，并请求该权限。如果所请求的权限不可用，则 
<B>Demand</B> 方法会抛出 <B>SecurityException</B>。其结果是，此代码会被包装进 try 代码块之中。如果 
<B>Demand</B> 方法成功，<B>HasFullTrust</B> 则返回 
<B>true</B>。如果出现异常而失败，<B>HasFullTrust</B> 则返回 <B>false</B>。</P>
<P>还有一件有趣的事件需要注意，如果检查完全信任失败，则应用程序将会试图显示一个消息框。实际上还有一个控制是否可以显示消息框的权限。如果没有设置 
<B>SafeSubWindows</B> 的 
UIPermission，则在应用程序试图显示消息框时会收到安全异常。有几种方法可以解决这个问题。一种是捕获产生的异常并退出程序，但是这样的话用户不知道何处出现错误。他们会在 
Microsoft Windows 资源管理器中双击该应用程序，但不会有任何响应（或者更确切地讲是看不到任何响应。Windows 
会在后台启动该应用程序，然后在尚未显示任何可见线索之前退出。）因此，我将程序集对此 <B>SafeSubWindows</B> 
权限的要求标记为最低限度。</P><PRE>[assembly:UIPermission(SecurityAction.RequestMinimum,  
Window=UIPermissionWindow.SafeSubWindows)]
</PRE>
<P>这样，如果程序集未被授予此最低权限，CLR 
将会显示其自有对话框，通知用户因缺少必要的权限而无法加载应用程序。如果授予了此最低权限，则会加载应用程序，同时我的逻辑会检查 <B>Main</B> 
方法所取得的完全信任。</P>
<DIV><A 
href="http://msdn.microsoft.com/zh-cn/library/aa480686.aspx#mainSection"><IMG 
alt="" 
src="Microsoft Sudoku：优化 UMPC 应用程序的点触和手写操作.files/Aa480686.arrow_px_up(zh-cn,MSDN.10).gif" 
border=0> </A><A 
href="http://msdn.microsoft.com/zh-cn/library/aa480686.aspx#mainSection">返回页首</A> 
<BR></DIV>
<H3 id=E06B0AA>处理严重异常错误</H3>
<P><B>Main</B> 方法首先查看完全信任，然后验证 Microsoft.Ink 程序集可用并且识别程序已安装后，会调用另外一个方法 
<B>MainInternal</B>，该方法用于完成主窗体的设置和启动等核心工作。<B>MainInternal</B> 
激活应用程序的主消息循环；因此，游戏中所有来自用户界面交互的未处理异常均从 <B>MainInternal</B> 
向外传播。为确保正确地记录这些异常（用于诊断和调试），<B>Main</B> 方法会捕获所有这些异常，将其记录然后退出。</P><PRE>try
{
return MainInternal() ? 0 : 1;
}
catch(Exception exc)
{
ShutdownOnException(new UnhandledExceptionEventArgs(exc, false));
return 1;
}
catch
{
ShutdownOnException(new UnhandledExceptionEventArgs(null, false));
return 1;
}
</PRE>
<P>我使用 <B>ShutdownOnException</B> 方法来记录严重错误条件，以确保应用程序能够尽快关闭。</P><PRE>internal static void ShutdownOnException(UnhandledExceptionEventArgs e)
{  
try
  {
string message = (e.ExceptionObject != null) ?
e.ExceptionObject.ToString() : 
ResourceHelper.ShutdownOnError;
EventLog.WriteEntry(ResourceHelper.DisplayTitle, message,  
EventLogEntryType.Error);
  }
catch {}

try
  {
MessageBox.Show(ResourceHelper.ShutdownOnError,  
ResourceHelper.DisplayTitle, MessageBoxButtons.OK,  
MessageBoxIcon.Hand, MessageBoxDefaultButton.Button1);
  } 
catch {}
  
if (!e.IsTerminating) Environment.Exit(1);
}
</PRE>
<P>在使用 .NET Framework 2.0 中提供的 <A id=ctl00_MTCS_main_ctl05 
onclick="javascript:Track('ctl00_MTCS_main_ctl00|ctl00_MTCS_main_ctl05',this);" 
href="http://go.microsoft.com/fwlink/?LinkId=63624">System.Environment.FailFast</A> 
方法之后，可以很好的完成大多数操作，但是此实现是基于 .NET Framework 1.1 的。</P>
<P><B>ShutdownOnException</B> 方法接受描述何处出现错误的 System.UnhandledExceptionEventArgs 
参数。该方法首先尝试记录有关异常的信息（包括堆栈跟踪），使用 System.Diagnostics.EventLog.WriteEntry 
静态方法将该信息写入应用程序事件日志中。然后会显示消息框，通知用户发生了意外。最后，如果应用程序仍未关闭，<B>Environment.Exit</B> 
方法会终止运行。</P>
<P>综上所述，仅在主 UI 线程出现未处理异常时调用 
<B>ShutdownOnException</B>。其他线程出现严重异常时怎么办？为解决这一问题，<B>MainInternal</B> 
方法所要做的第一件事，就是将事件处理器与 AppDomain 的当前 <B>UnhandledException</B> 事件以及 Windows Form 的 
Application.ThreadException 事件联系起来。</P><PRE>AppDomain.CurrentDomain.UnhandledException +=  
new UnhandledExceptionEventHandler(
CurrentDomain_UnhandledException);
Application.ThreadException +=  
new ThreadExceptionEventHandler(Application_ThreadException);
</PRE>
<P>这些事件的事件处理器委托给 <B>ShutdownOnException</B>。</P><PRE>private static void Application_ThreadException(
object sender, ThreadExceptionEventArgs e)
{
ShutdownOnException(new UnhandledExceptionEventArgs(
e.Exception, false));
}
private static void CurrentDomain_UnhandledException(
object sender, UnhandledExceptionEventArgs e)
{
ShutdownOnException(e);
}
</PRE>
<P>利用这些代码，用户在应用程序使用过程中遇到的所有故障或严重异常都会被记录下来，产品故障的调试工作将会更加轻松。请注意，在 .NET Framework 
2.0 之下，这些工作大多数都是自动完成的。逐一解决运行过程中所有未处理异常之后，Dr. Watson 开始转储应用程序以便于日后的分析和调试。</P>
<DIV><A 
href="http://msdn.microsoft.com/zh-cn/library/aa480686.aspx#mainSection"><IMG 
alt="" 
src="Microsoft Sudoku：优化 UMPC 应用程序的点触和手写操作.files/Aa480686.arrow_px_up(zh-cn,MSDN.10).gif" 
border=0> </A><A 
href="http://msdn.microsoft.com/zh-cn/library/aa480686.aspx#mainSection">返回页首</A> 
<BR></DIV>
<H3 id=E03B0AA>单实例应用程序</H3>
<P>许多应用程序，尤其是 Tablet PC 
应用程序，大都是单实例应用程序。单实例应用程序是指，始终只有一个执行该应用程序的进程在运行的应用程序，无论是在特定桌面应用还是整个计算机中均是如此，取决于您对“单实例”的定义以及对应用程序的需求。对于 
Sudoku，我想要计算机上的每个用户每次只能运行 Sudoku 的一个实例。如果使用 Microsoft Visual Studio 2005 和 .NET 
Framework 2.0 来实现 Sudoku，则单实例支持已经可用。但我使用的是 .NET Framework 
1.1，所以还需其他工作才能实现单实例支持。有关单实例支持的详细信息，请参阅 <A id=ctl00_MTCS_main_ctl06 
onclick="javascript:Track('ctl00_MTCS_main_ctl00|ctl00_MTCS_main_ctl06',this);" 
href="http://go.microsoft.com/fwlink/?LinkId=63625">.NET Matters column in MSDN 
Magazine's September, 2005 issue</A>（英文）。</P>
<P>我的单实例支持内置于 <B>SingleInstance</B> 类中，位于 Utilities 文件夹的 SingleInstance.cs 
文件中。<B>MainInternal</B> 按照下列方式使用 <B>SingleInstance</B>。</P><PRE>using(SingleInstance si = new SingleInstance())
{
if (si.IsFirst)
  {
//创建主窗体并运行应用程序
    ...

//成功完成游戏
return true;
  }
else
  {
//不是第一个 Sudoku 实例... 显示另外一个
si.BringFirstToFront();
return false;
  }
}
</PRE>
<P>我首先创建一个新的类实例并查询其 <B>IsFirst</B> 
属性，以确定该应用程序当前是否有其他实例正在运行。如果这是第一个实例，则应用程序继续正常运行，创建并显示应用程序的主窗体。如果这不是第一个实例，<B>BringFirstToFront</B> 
方法会把 Sudoku 应用程序第一个实例的窗口转至前台，然后退出第二个实例。</P>
<P>单实例功能需要某种进程间通信 (IPC) 
的方式，这是由于一个实例需要通知其他实例已经有实例存在，不应继续加载第二个实例。<B>SingleInstance</B> 类实际上依赖两个不同的 IPC 
机制：进程间同步原语和内存映射文件。</P>
<P>创建单纯的单实例功能十分简单，只需几行代码即可完成。</P><PRE>static void Main()
{
string mutexName = "2ea7167d-99da-4820-a931-7570660c6a30";
bool grantedOwnership;
using(Mutex singleInstanceMutex =  
new Mutex(true, mutexName, out grantedOwnership)
  {
if (grantedOwnership)
    {
... //此处为核心代码
    }
  }
}
</PRE>
<P>当双线程或更多线程同时访问共享资源时，您需要有同步机制来确保在某一时间点只有一个线程可以访问该资源。对于我们而言，“资源”的逻辑含义要比其物理上的含义广阔的多。它是保证正在运行的应用程序只有一个实例的能力。所谓 
<I>mutex</I>，如在 System.Threading.Mutex 
类中所实现的，是在某一时间点，仅向一个线程授予独占访问权限的同步原语。如果一个线程获得 <B>Mutex</B>，则暂停其他请求相同 <B>Mutex</B> 
的线程，直到第一个线程释放 <B>Mutex</B> 为止。或者，在某一线程等待资源时不将其暂停，而是在其尝试获得 <B>Mutex</B> 时被告知该 
<B>Mutex</B> 已被其他线程占用。</P>
<P>此处，以 GUID 名称创建 <B>Mutex</B>（所有使用此类代码的不同应用程序将会选择其唯一的名称）。在您使用某一名称创建 
<B>Mutex</B> 之后，就可以用来同步存在于不同进程之间的线程，而每一进程都可以根据其预设和已知的名称来引用 
<B>Mutex</B>。<B>Mutex</B> 的一个构造函数不仅接受名称，还接受 <B>out</B> 布尔参数，该参数用于指示 <B>Mutex</B> 
是否实际可得。这样就可以轻松创建单实例功能。该代码会创建 <B>Mutex</B> 并检查其是否可用。如果 <B>Mutex</B> 
可用，则该应用程序的实例就是第一个实例，可以继续进行。如果 <B>Mutex</B> 不可用，那么它就是第二个实例，应该退出。</P>
<P>在内部，<B>SingleInstance</B> 围绕相同的原则构建，但是稍有不同。首先，<B>SingleInstance</B> 不使用硬编码 
GUID，而是基于当前应用程序中输入程序集的身份生成 ID。</P><PRE>private static string ProgramID
{
get
  {
Assembly asm = Assembly.GetEntryAssembly();
Guid asmId = Marshal.GetTypeLibGuidForAssembly(asm);
return asmId.ToString("N") + asm.GetName().Version.ToString(2);
  }
}
</PRE>
<P>ID 是程序集类型库 GUID 和程序集版本的组合。按照下列方式使用 ID 来构造 <B>Mutex</B> 名称。</P><PRE>string mutexName = "Local\\" + ProgramID + "_Lock";
</PRE>
<P>如同上一个简单的单实例示例，我的 <B>SingleInstance</B> 类构造函数对 <B>Mutex</B> 
进行实例化，尽管是将其存储在成员变量而非本地变量中。</P><PRE>_singleInstanceLock = new Mutex(true, mutexName, out _isFirst);
</PRE>
<P>构造函数的第三个参数是布尔变量，该变量用于存储此 <B>Mutex</B> 实例能否获得锁定，然后可以通过 <B>IsFirst</B> 
属性访问该布尔值。</P><PRE>public bool IsFirst { get { return _isFirst; } }
</PRE>
<P><B>SingleInstance</B> 实现的 <B>IDisposable.Dispose</B> 会关闭 <B>Mutex</B>。</P>
<P>如果这正是所要求的功能，那么我的工作就基本上完成了。但是，在试图启动第二个实例时，许多单实例应用程序还会把现有实例转至前台。这一功能要求可在进程间传递附加信息。尤其是，除了检测是否已经存在实例外，第二个实例必须能够确定应将哪个进程转至前台。我注意到，一些应用程序是通过搜索桌面上具有特定名称的所有顶层窗口来完成这一任务，但这种方法不太可靠。我采用的是另外一种方式，<I>内存映射文件</I>。</P>
<P>对于我们的情况，可以将内存映射文件看作一种内存共享方式。这允许一个进程可以向内存中写入信息，供其他进程读取。特别是，应用程序的第一个实例可向内存映射文件中写入其进程 
ID，然后第二个实例可以读取该进程 ID 并使用该 ID 将主实例转至前台。</P><PRE>private void WriteIDToMemoryMappedFile(uint id)
{
_memMappedFile = new HandleRef(this,  
NativeMethods.CreateFileMapping(new IntPtr(-1), IntPtr.Zero,  
PAGE_READWRITE, 0, IntPtr.Size, _memMapName));
if (_memMappedFile.Handle != IntPtr.Zero &amp;&amp;  
_memMappedFile.Handle != new IntPtr(-1))
  {
IntPtr mappedView = NativeMethods.MapViewOfFile(
_memMappedFile.Handle, FILE_MAP_WRITE, 0, 0, 0);
try
    {
if (mappedView != IntPtr.Zero)  
Marshal.WriteInt32(mappedView, (int)id);
    }
finally
    {
if (mappedView != IntPtr.Zero)  
NativeMethods.UnmapViewOfFile(mappedView);
    }
  }
}

private uint ReadIDFromMemoryMappedFile()
{
IntPtr fileMapping = NativeMethods.OpenFileMapping(
FILE_MAP_READ, false, _memMapName);
if (fileMapping != IntPtr.Zero &amp;&amp; fileMapping != new IntPtr(-1))
  {
try
    {
IntPtr mappedView = NativeMethods.MapViewOfFile(
fileMapping, FILE_MAP_READ, 0, 0, 0);
try
      {
if (mappedView != IntPtr.Zero) 
return (uint)Marshal.ReadInt32(mappedView);
      }
finally
      {
if (mappedView != IntPtr.Zero)  
NativeMethods.UnmapViewOfFile(mappedView);
      }
    }
finally { NativeMethods.CloseHandle(fileMapping); }
  }
return 0;
}
</PRE>
<P>类似于进程间的互斥锁，也可以命名内存映射文件，以便多个进程通过该名称访问。可使用 kernel32.dll 提供的 CreateFileMapping 
函数创建内存映射文件。然后使用同样由 kernel32.dll 提供的 MapViewOfFile 
函数将内存映射文件映射到当前地址空间。<B>MapViewOfFile</B> 为内存提供起始地址，然后与来自 
System.Runtime.InteropServices.Marshal 
类的方法一起使用，读取内存中的数据以及向内存中写入数据。如果已经创建了内存映射文件，您可以使用来自 kernel32.dll 的 OpenFileMapping 
函数打开该文件。</P><PRE>[DllImport("Kernel32", CharSet=CharSet.Auto, SetLastError=true)]
internal static extern IntPtr CreateFileMapping(IntPtr hFile,  
IntPtr lpAttributes, int flProtect, int dwMaxSizeHi,  
int dwMaxSizeLow, string lpName);

[DllImport("Kernel32", CharSet=CharSet.Auto, SetLastError=true)]
internal static extern IntPtr OpenFileMapping(int dwDesiredAccess,  
[MarshalAs(UnmanagedType.Bool)] bool bInheritHandle,  
string lpName);

[DllImport("Kernel32", CharSet=CharSet.Auto, SetLastError=true)]
internal static extern IntPtr MapViewOfFile(IntPtr hFileMapping,  
int dwDesiredAccess, int dwFileOffsetHigh, int dwFileOffsetLow,  
int dwNumberOfBytesToMap);

[DllImport("Kernel32", CharSet=CharSet.Auto, SetLastError=true)]
[return:MarshalAs(UnmanagedType.Bool)]
internal static extern bool UnmapViewOfFile(IntPtr pvBaseAddress);

[DllImport("kernel32.dll", SetLastError=true)]
[return:MarshalAs(UnmanagedType.Bool)]
internal static extern bool CloseHandle(IntPtr handle);
 
</PRE>
<P><B>SingleInstance</B> 构造函数创建 <B>Mutex</B> 
并验证其是否为第一个实例。如果是第一个实例，<B>SingleInstance</B> 构造函数使用 
<B>WriteIDToMemoryMappedFile</B> 方法来存储自己的进程 ID，以供第二个实例随后检索之用。当第二个实例开始运行并调用 
<B>BringFirstToFront</B> 方法时，第二个实例首先使用 <B>ReadIDFromMemoryMappedFile</B> 
来访问主实例的进程 ID。可将该进程 ID 与 user32.dll 提供的 <B>ShowWindowAsync</B> 和 
<B>SetForegroundWindow</B> 函数结合使用，将主实例转至前台。</P><PRE>public void BringFirstToFront()
{
if (!_isFirst)
  {
uint processID = ReadIDFromMemoryMappedFile();
if (processID != 0)
    {
const int SW_SHOWNORMAL = 0x1;
const int SW_SHOW = 0x5;
IntPtr hwnd = new ProcessInfo(processID).MainWindowHandle;
if (hwnd != IntPtr.Zero)
      {
int swMode = NativeMethods.IsIconic(new HandleRef(
this, hwnd)) ?SW_SHOWNORMAL :SW_SHOW;
NativeMethods.ShowWindowAsync(hwnd, swMode);
NativeMethods.SetForegroundWindow(new HandleRef(
this, hwnd));
      }
    }
  }
}
</PRE>
<DIV><A 
href="http://msdn.microsoft.com/zh-cn/library/aa480686.aspx#mainSection"><IMG 
alt="" 
src="Microsoft Sudoku：优化 UMPC 应用程序的点触和手写操作.files/Aa480686.arrow_px_up(zh-cn,MSDN.10).gif" 
border=0> </A><A 
href="http://msdn.microsoft.com/zh-cn/library/aa480686.aspx#mainSection">返回页首</A> 
<BR></DIV>
<H3 id=E0ZB0AA>Sudoku 算法</H3>
<P>本部分将对 Sudoku 游戏特有的应用程序特征方面进行详细阐述。</P>
<P>维护游戏状态</P>
<P>编写这样的应用程序时，我会首先确定尽可能少的一组功能，将其作为以后工作的基础。具体到 Sudoku，就是首先创建必要的数据结构，以保存游戏状态。</P>
<P>可以简单地将填数游戏表示为一个二维数组。每个单元格必须能存储一个数字，或者为空。.NET Framework 2.0 
能够提供<I>可以为空的类型</I>这种形式，从而给我们带来了一种良好的解决方案。.NET Framework 2.0 
中“可以为空的类型”允许值类型也为空值，这代表着它使用了一种新的泛型结构：<B>Nullable&lt;T&gt;</B>。<B>Nullable&lt;T&gt;</B> 
可以在内部保存两个值，一个是类型 <B>T</B>（在这里，<B>T</B> 
是一个泛型类型参数，可以为任意的值类型，但不能为另一个可以为空的类型），存储的是实例的值；另一个是布尔型，表明该实例是否有值。.NET Framework 
1.1 中不能使用泛型，但我还有其他办法。对于几个想要支持空值的类型，我可以创建自己的替代类型，如 <B>NullableByte</B> 和 
<B>NullableInt</B>。每种替代类型都是一种包含相关类型的值（如 <B>byte</B> 适用于 
<B>NullableByte</B>，<B>int</B> 适用于 
<B>NullableInt</B>）和一个布尔值的结构，布尔值表示整数值中是否含有可用值。这虽然没有 .NET Framework 2.0 
的泛型解决方案那么高明，从 C# 2.0 提供对 <B>Nullable&lt;T&gt;</B> 
的语法支持之后更是如此，但就实现我的目标而言，已经足够了。游戏网格中的每个单元格或者为空，或者是一个较小的数值，所以我可以按照一个 
<B>NullableBytes</B> 数组来保存网格。</P><PRE>[Serializable]
public sealed class PuzzleState :ICloneable, IEnumerable
{
private NullableByte[,] _grid;
private readonly byte _boxSize;
private readonly byte _gridSize;
  ...
}
</PRE>
<P><B>PuzzleState</B> 类包含几条信息，包括网格的大小和内容。目前的实现程序仅支持 9x9 Sudoku 
网格，但是经完善后，该应用程序可以获得扩展，能够支持更大的网格，甚至是非正方形网格。 </P>
<P><B>PuzzleState</B> 最重要的特征之一是 ICloneable<B> </B>接口的实现。该接口实现后，您可以轻松地进行 
<B>PuzzleState</B> 
实例的深拷贝，将其深入应用到解决方案的其他各个方面，如游戏解法、游戏生成，甚至还可以用在与游戏相关的功能中，如撤消支持。</P><PRE>object ICloneable.Clone() { return this.Clone(); }
public PuzzleState Clone() { return new PuzzleState(this); }
private PuzzleState(PuzzleState state)
{
  _boxSize = state._boxSize;
  _gridSize = state._gridSize;
  _grid = (NullableByte[,])state._grid.Clone();
  ...
}
</PRE>
<DIV><A 
href="http://msdn.microsoft.com/zh-cn/library/aa480686.aspx#mainSection"><IMG 
alt="" 
src="Microsoft Sudoku：优化 UMPC 应用程序的点触和手写操作.files/Aa480686.arrow_px_up(zh-cn,MSDN.10).gif" 
border=0> </A><A 
href="http://msdn.microsoft.com/zh-cn/library/aa480686.aspx#mainSection">返回页首</A> 
<BR></DIV>
<H3 id=E0WB0AA>分析和完成填数游戏</H3>
<P>完成 Sudoku 填数游戏的过程实际上是一个特殊的搜索过程，因此，我可以将标准的搜索算法运用到游戏当中。一种常见的搜索算法是深度优先搜索 
(DFS)，在大学的数据结构和算法课程中，这通常是首先要学的算法之一。搜索时，有的实例需要探寻多条路径，在 DFS 
中，要对其中一条路径进行整体探寻，包括由该路径引出的所有路径，然后再依次检查其他路径。虽然可以使用显式堆栈的方式执行 
DFS，但我们通常使用递归法。假设在一个树节点数据结构中，除一列子节点外，还存储有一个值。可以通过如下方式对树内一个具体值执行 DFS 过程：</P><PRE>public class TreeNode&lt;T&gt; where T :IComparable
{
public TreeNode(T value) { Value = value; }
public T Value;
public List&lt;TreeNode&lt;T&gt;&gt; Children = new List&lt;TreeNode&lt;T&gt;&gt;();

public TreeNode&lt;T&gt; DfsSearch(T v)
  {
if (Value.CompareTo(v) == 0) return this;
foreach (TreeNode&lt;T&gt; child in Children)
    {
TreeNode&lt;T&gt; rv = child.DfsSearch(v);
if (rv != null) return rv;
    }
return null;
  }
}
</PRE>
<P>在 <B>TreeNode&lt;T&gt;</B> 上调用 <B>DfsSearch</B> 
以检查节点内存储的值是否与要搜索的值相同。如果相同，即返回当前的节点。如果不同，则调用每个子节点的 <B>DfsSearch</B> 
方法，检查要搜索的值是否存在于该子节点的子树上。通过这种方式，我可以搜索整个树结构。 </P>
<P>您可以用相同的方式搜索 Sudoku 
的解法。将游戏盘的一个具体状态当作树内的一个节点。以该状态为起点，我可以在网格中的空单元格内输入一个数字，转到其他“子”状态。通过这种方式，我可以从每个状态达到一定数量的子状态（每在一个空单元格中输入一个值，即得到一个状态）。对于 
9x9 的网格，最多有 729 个子状态，这是一个确定的数值。因此，我可以使用很少的 DFS 类似代码对 Sudoku 游戏的解法执行搜索。</P><PRE>static PuzzleState DfsSolve(PuzzleState state)
{
switch (CheckSolution(state))
  {
case PuzzleStatus.Solved:return state;
case PuzzleStatus.CannotBeSolved:return null;
default:
NullablePoint cell = FindFirstEmptyCell(state);
if (cell == null) return null;
for (byte n = 0; n &lt; state.GridSize; n++)
      {
state[cell.Value] = n;
PuzzleState result = DfsSolve(state);
if (result != null) return result;
      }
state[cell.Value] = null;
return null;
  }
}
</PRE>
<P>这种方法与 Sudoku 实现程序的部署方法类似，检查 <B>PuzzleState</B> 
并确定是否已完成；当前是否处于无效状态（指存在冲突）并且已确定无法完成；或者是否仍有一个处于进程中的未完成游戏。执行这一检查与 
<B>TreeNode&lt;T&gt;</B> 的 <B>DfsSearch</B> 
检查类似，主要是看已找到的节点中是否包含正确值，以及检查是否存在解的位置。如果找到一个解，即将其返回。如果找到的是一个无效状态，则说明在某处出错，需要原路返回。如果游戏仍在继续，需要在一个空单元格中输入一个数字。找出首个为空的单元格，依次输入每个可能值。如果该游戏有解，其中某个值一定是正确的。我在每个单元格中输入一个数字，然后递归调用 
<B>DfsSolve</B>。只要游戏有解，最后就一定能够找到。</P>
<P>遗憾的是，这种方法有一个致命的缺点：完成游戏需要相当长的时间。对于 9x9 的游戏盘，需要为每个单元格试填的值最高为九个。对于需要填充 81 
个单元格的游戏盘，需要试填的盘数高达 
9<SUP>81</SUP>。即使速度达到每十亿分之一秒处理一个单元格中的一个数字，结果也很恐怖，我和各位读者都不可能在有生之年看到游戏结束！事实上，找到解之前，太阳都可能已经不复存在了。问题的症结在于，这种方法需要搜索整个搜索空间，即使可以通过许多办法对搜索树进行“修枝剪叶”，例如限制搜索必须执行的寻找数量，但对于缩短时间来说，这些都是杯水车薪。我们玩 
Sudoku 游戏时都知道有许多步骤是不必进行的，那为什么还要让计算机去试呢？所以，丢掉它们！</P>
<P>我们可以通过许多技巧让计算机省去一些步骤，其中最简单的一种是，通知计算机某个具体的单元格中一定不会含有哪些数字。这样，计算机进行强力搜索时只需要考虑剩余的可能数字。 
</P>
<P>对网格内的每个单元格，我会保留一个位数组。这里有一种思路，即通过检查其他自动设置数字，或者单元格的列、行和框中已“给出”的数字，为每个单元格排除一定量的可能数字；已出现的任何数字都不可能是该单元格中应有的数字。原则上，位数组是一组开关，通过打开或关闭来维护一组项目的布尔值。在.NET 
Framework 中表示位数组的最简便方法是使用 System.Collections.BitArray 
类。但是，我在实现程序中使用该类并进行了一些配置文件测试之后，我发现创建适合具体要求的新的实现程序更为方便。所以，我创建了自己的实现程序 
<B>FastBitArray</B>，它位于 Collections 目录下的 <B>FastBitArray.cs</B> 
文件中。（如果您编写过位桶，就会对该实现程序非常熟悉。）因为 Sudoku 实现程序不需要处理大的值（假定其仅支持 9x9 
游戏盘），<B>FastBitArray</B> 就可以对单一无符号整数进行限制，只允许最多保留 32 个布尔值。通过简单的位操作即可实现读写操作。</P><PRE>public bool Get(int index)  
{
return (_bits &amp; (1u &lt;&lt; index)) != 0;
}
public void Set(int index, bool value)  
{
bool curVal = Get(index);
if (value &amp;&amp; !curVal)  
  {
_bits |= (1u &lt;&lt; index);
_countSet++;
  }
else if (!value &amp;&amp; curVal)
  {
_bits &amp;= ~(1u &lt;&lt; index);
_countSet--;
  }
}
</PRE>
<P>之后，我会保留一个 <B>FastBitArray</B> 实例的二维数组，与包含网格中各单元格的值的二维数组相对应。</P>
<P>了解单元格中的可能值可以使 <B>DfsSolve</B> 
方法更快地发挥作用。我可以将搜索限制在各单元格的备选值中，而不必像以前那样，在每个单元格中试填所有值。此外，通过 <B>DfsSolve</B> 
实现程序查找并填充找到的第一个空单元格，这其实是一个误导。在具体单元格内试填的值中只有一个是正确的，这就给了我们一种启示：如果从我们知道的备选数字最少的单元格开始，将能够省去大量的搜索工作。因此，要进一步完善 
<B>DfsSolve</B>，可以在所有空单元格中找出备选数字最少的单元格。在 <B>DfsSolve</B> 
中每设定一个单元格，随即更新备选数字的数组，删除已经为单元格设置的备选数字。</P>
<P>创建 <B>DfsSolve</B> 是为了说明与解释，实际上，我用在 Sudoku 中的实现程序与之非常相似。在 Solver.cs 文件中，我的 
<B>Solver</B> 类中有这两个核心方法的简略版本。请留意它们与 <B>DfsSolve</B> 的相似之处，以及我提到的补充建议。</P><PRE>private static SolverResults SolveInternal(
PuzzleState state, SolverOptions options)
{
state = state.Clone();

FastBitArray [][] possibleNumbers =  
FillCellsWithSolePossibleNumber(state,  
options.EliminationTechniques);

switch (state.Status)
  {
case PuzzleStatus.Solved:
case PuzzleStatus.CannotBeSolved:
return new SolverResults(state.Status, state, 0);
default:
if (options.AllowBruteForce)
      {
SolverResults results = BruteForceSolve(
state, options, possibleNumbers);
return results;
      } 
else return new SolverResults(
PuzzleStatus.CannotBeSolved, state, 0, null);
  }
}

private static SolverResults BruteForceSolve(PuzzleState state,  
SolverOptions options, FastBitArray [][] possibleNumbers)
{
//找到备选数字最少的单元格
ArrayList bestGuessCells = new ArrayList();
int bestNumberOfPossibilities = state.GridSize + 1;
for (int i = 0; i &lt; state.GridSize; i++)
  {
for (int j = 0; j &lt; state.GridSize; j++)
    {
int count = possibleNumbers[i][j].CountSet;
if (!state[i, j].HasValue)
      {
if (count &lt; bestNumberOfPossibilities)
        {
bestNumberOfPossibilities = count;
bestGuessCells.Clear();
bestGuessCells.Add(new Point(i, j));
        }
else if (count == bestNumberOfPossibilities)
        {
bestGuessCells.Add(new Point(i, j));
        }
      }
    }
  }

//选择一个
SolverResults results = null;
if (bestGuessCells.Count &gt; 0)
  {
Point bestGuessCell = (Point)bestGuessCells[
RandomHelper.GetRandomNumber(bestGuessCells.Count)];
FastBitArray possibleNumbersForBestCell = possibleNumbers[
bestGuessCell.X][bestGuessCell.Y];
for(byte p=0; p&lt;possibleNumbersForBestCell.Length; p++)
    {
if (possibleNumbersForBestCell[p])
      {
PuzzleState newState = state;
newState[bestGuessCell] = p;
SolverOptions tempOptions = options.Clone();
if (results != null)  
        {
tempOptions.MaximumSolutionsToFind = (uint)(
tempOptions.MaximumSolutionsToFind.Value a€“  
results.Puzzles.Count);
        }

//重复
SolverResults tempResults =  
SolveInternal(newState, tempOptions);

if (tempResults.Status == PuzzleStatus.Solved)
        {
if (results != null &amp;&amp; results.Puzzles.Count &gt; 0)
          {
results.Puzzles.AddRange(tempResults.Puzzles);
          }
else
          {
results = tempResults;
results.NumberOfDecisionPoints++;
          }
if (options.MaximumSolutionsToFind.HasValue &amp;&amp;
results.Puzzles.Count &gt;=  
options.MaximumSolutionsToFind.Value)  
return results;
        }

newState[bestGuessCell] = null;
      }
    }
  }
return results != null ? results :new    
SolverResults(PuzzleStatus.CannotBeSolved, state, 0, null);
}
</PRE>
<P>这里要完成许多操作。<B>SolveInternal</B> 方法从复制 <B>PuzzleState</B> 
开始，然后在副本而非原件上进行操作。这样，调用程序提供的 <B>PuzzleState</B> 就不会受到 <B>Solver</B> 启动操作的影响。然后使用 
<B>FillCellsWithSolePossibleNumber</B> 
分析当前的游戏盘，找出只有一个备选数字的单元格，然后填入数字（后面我会讲解这一方法）。这时，如果找到了游戏盘的解法或者被确定为无解，该方法即会返回。否则，使用强力搜索来填充一个单元格。备选数字最少的单元格将会成为下一个要填入数字的单元格（如果备选数字最少的单元格不止一个，则随机选择一个）。填入数字后，系统将调用 
<B>SolveInternal</B> 重复这一过程，进程重新开始，已填入多个单元格的情况除外。</P>
<P>您会注意到，<B>SolverOptions</B> 类的实例在求解操作中，会推动某些决定的产生。以下代码可显示 <B>SolverOptions</B> 
公共接口：</P><PRE>public sealed class SolverOptions :ICloneable
{
public SolverOptions();
public NullableUint MaximumSolutionsToFind { get; set; }
public TechniqueCollection EliminationTechniques { get; }
public bool AllowBruteForce { get; set; }
public SolverOptions Clone();
}
</PRE>
<P><B>MaximumSolutionsToFind</B> 可以使 <B>Solver</B> 
的客户端确定需要尝试解的数目；只有这些解全部找到后，或求解器确定请求的数字无法找到时，求解器才会终止。请注意，<B>MaximumSolutionsToFind</B> 
是一种自定义 <B>NullableUint</B> 类型；如果是 <B>null</B>，求解器会查找所有可能的无界解。但对该值的最普遍用法是，将其设置为 
<B>1</B> 或 
<B>2</B>。通过对一个解的搜索，我可以迅速确定该游戏是否可解。通过搜索两个解，我可以确定游戏有一个解还是两个解（如果求解器找到两个解，即使还能找到更多的解，但至少我已经确定，该游戏不止有一个解）。<B>AllowBruteForce</B> 
属性决定了求解器是否允许强力搜索，还是只能使用 <B>FillCellsWithSolePossibleNumber</B> 
方法填充数字。<B>FillCellsWithSolePossibleNumber</B> 使用 <B>EliminationTechniques</B> 
属性为每个单元格指定可能的数字。（我会在生成游戏中讲解 <B>AllowBruteForce</B> 和 
<B>FillCellsWithSolePossibleNumber</B>。）</P>
<P>讨论原型 <B>DfsSolve</B> 时，我会顺便解释一下 <B>CheckSolution</B>。在实际的 Sudoku 
实现程序中，<B>CheckSolution</B> 由 <B>PuzzleState.Status</B> 
代替，后者的功能是分析当前的游戏状态，并返回信息,说明游戏是已完成、未完成还是处于某种中间状态。 </P><PRE>public PuzzleStatus Status
{
get
  {
if (_status == PuzzleStatus.Unknown)  
_status = AnalyzeSolutionStatus();
return _status;
  }
}
private PuzzleStatus AnalyzeSolutionStatus()
{
FastBitArray numbersUsed = new FastBitArray(_gridSize);

//确定各列中无重复数字
for (int i = 0; i &lt; _gridSize; i++)
  {
numbersUsed.SetAll(false);
for (int j = 0; j &lt; _gridSize; j++)
    {
if (_grid[i, j].HasValue)
      {
int value = _grid[i, j].Value;
if (numbersUsed[value])  
return PuzzleStatus.CannotBeSolved;
numbersUsed[value] = true;
      }
    }
  }

//确保各行中无重复数字
for (int j = 0; j &lt; _gridSize; j++)
  {
numbersUsed.SetAll(false);
for (int i = 0; i &lt; _gridSize; i++)
    {
if (_grid[i, j].HasValue)
      {
int value = _grid[i, j].Value;
if (numbersUsed[value])  
return PuzzleStatus.CannotBeSolved;
numbersUsed[value] = true;
      }
    }
  }

//确保各框中无重复数字
for (int boxNumber = 0; boxNumber &lt; _gridSize; boxNumber++)
  {
numbersUsed.SetAll(false);
int boxStartX = (boxNumber / _boxSize) * _boxSize;
for (int x = boxStartX; x &lt; boxStartX + _boxSize; x++)
    {
int boxStartY = (boxNumber % _boxSize) * _boxSize;
for (int y = boxStartY; y &lt; boxStartY + _boxSize; y++)
      {
if (_grid[x, y].HasValue)
        {
int value = _grid[x, y].Value;
if (numbersUsed[value])  
return PuzzleStatus.CannotBeSolved;
numbersUsed[value] = true;
        }
      }
    }
  }

//现在指出这是已完成的填数游戏 
//还是仍在进行中（根据是否存在漏洞）
for (int i = 0; i &lt; _gridSize; i++)
  {
for (int j = 0; j &lt; _gridSize; j++)
    {
if (!_grid[i, j].HasValue) return PuzzleStatus.InProgress;
    }
  }

//如果能运行到这一步，则表明该状态有解！  
return PuzzleStatus.Solved;
}
</PRE>
<P><B>PuzzleState.Status</B> 通过几个步骤分析当前的解。它检查该状态的 <B>PuzzleStatus</B> 
是否已经过计算并被缓存。如果是，<B>PuzzleStatus</B> 会返回缓存值（对游戏的任何更改都会使缓存无效）。假定该状态的 
<B>PuzzleStatus</B> 确实已经过计算，<B>Status</B> 就会使用 <B>AnalyzeSolutionStatus</B> 
方法查看所有列、行或框中当前是否存在重复数字。如果存在重复，那么很显然，游戏无法完成。然后，系统会查看网格中是否还存在空单元格。如果有，游戏继续进行。如果没有，游戏即告完成。请注意，<B>AnalyzeSolutionStatus</B> 
不能指出正在进行的游戏是否可以被完成，只能指出游戏当前是否已完成、确定不可解，或者可能有解。您可以看到，<B>Solver</B> 
能够执行搜索，确定游戏的真实可解性。</P>
<P>这也是 <B>Solver</B> 的基本功能。公共 <B>Solve</B> 方法包括 <B>SolveInternal</B> 
方法，其他一些操作则用来追踪统计数据 - 如何完成游戏，游戏生成中哪条信息是有用信息。无需太多代码即可实现一个非常有用而且强大的类。</P>
<DIV><A 
href="http://msdn.microsoft.com/zh-cn/library/aa480686.aspx#mainSection"><IMG 
alt="" 
src="Microsoft Sudoku：优化 UMPC 应用程序的点触和手写操作.files/Aa480686.arrow_px_up(zh-cn,MSDN.10).gif" 
border=0> </A><A 
href="http://msdn.microsoft.com/zh-cn/library/aa480686.aspx#mainSection">返回页首</A> 
<BR></DIV>
<H3 id=E0TB0AA>生成游戏</H3>
<P>看起来，有待解决的最大难题应该是如何生成只有一个有效解法的随机填数游戏；而事实上，有已编好的所有其他代码做基础，编写这部分代码会相当容易。我相信，网上有足够多的算法可以用于生成游戏。但是我情愿选择自己的算法，原因如下：它很优秀，使用已为此实现的类和方法可以轻松编写代码；速度也相当快；最重要的一点是，它的效果不错。当然，如果您愿意，完全可以从附随代码中删除该生成器，代之以自己的算法。</P>
<P>支持这一算法的思路非常简单，由两部分组成。首先，需要创建一个完整和随机的 Sudoku 解法，也就是说，通过这一随机解法，网格中所有 81 
个单元格均填有数字，每行、每列和每个框中均含有不重复的 1 至 9 九个数字。乍看上去，这是一个无解的难题，但是我已经通过一定的代码解决了这一问题： 
<B>Solver.Solve</B>。请不要对此太过惊讶！您可以回顾一下我关于完成填数游戏和找到下一个要强力搜索的单元格的讨论，我将从备选数字最少的单元格中随机选择一个。因此，为创建一个随机而且完整的 
Sudoku 解法，我需要做的全部工作就是完成一个空网格。我在这里做了大量工作，因为这将是关键的一步。首次通过时，求解器将随机在 81 
个单元格中的一个填入数值，因为所有 81 个单元格都有相同数目的备选数字。在该单元格填入数字后，一些单元格的备选数字就会比其他单元格少，solver 
将会从这些单元格中随机选择一个。这样执行下去，将会生成一个随机解法。只要一两行代码，就能生成一个随机的有效解法。</P><PRE>PuzzleState newPuzzle = Solver.Solve(
new PuzzleState(), new SolverOptions()).Puzzle;
</PRE>
<P>上面就是要做的全部工作！尽管可能存在更为有效的方式，但是上述代码只需在便携式电脑上运行几毫秒，比我预想的还快。</P>
<P>算法的下一步是，全面展开并不断从单元格中删除数字，方法是随机选择网格中已填充的一个单元格并清除其中的数字。每删除一个，我就用求解器确定一次，看现在是否还有多种解法。很明显，原始的游戏盘只有一个解法，删除几个数字后，应该仍然只有一个解法。但是有些时候，清除了大量单元格的数字后，游戏盘会处在一个多可用方法完成的状态下，每种状态都通向不同的 
Sudoku 解法。这有悖于创建仅一种解法的游戏盘这一目标，因此必须避免出现这种状态。因此，每次删除后，<B>Solver</B> 
将通知我目前是否状态不明。那样的话，我可以撤消该删除步骤，以实现有效的游戏盘启动。如下为这一方法的核心，在 Generator.cs 文件的 
<B>Generator</B> 类中执行。</P><PRE>Point [] filledCells = GetRandomCellOrdering(newPuzzle);
int filledCellCount = filledCells.Length;
for(int filledCellNum=0;
filledCellNum &lt; filledCellCount &amp;&amp; 
newPuzzle.NumberOfFilledCells &gt; _options.MinimumFilledCells;  
filledCellNum++)
{
byte oldValue = newPuzzle[filledCells[filledCellNum]].Value;
newPuzzle[filledCells[filledCellNum]] = null;
SolverResults newResults = Solver.Solve(
newPuzzle, solverOptions);
if (!IsValidRemoval(newPuzzle, newResults))  
newPuzzle[filledCells[filledCellNum]] = oldValue;
}
</PRE>
<P><B>GetRandomCellOrdering</B> 方法以随机顺序在游戏中创建一个包含所有 81 
个单元格的列表。<B>IsValidRemoval</B> 
方法会进行检查，确定上一次删除是否遵守了所有规则，包括游戏只能有一个解法这一基本规则。您可以在该代码中看到，我按照 
<B>GetRandomCellOrdering</B> 返回的顺序对所有 81 个单元格进行循环操作。对于每个单元格，我的初衷是删除它，但如果根据 
<B>IsValidRemoval</B> 最终状态无效，我会将其恢复。</P>
<P>我的生成器执行的算法称作<I>贪心算法</I>。也就是说，它会执行发现的每个有效步骤，即使如果没有这样做，最终的整体结果也会更好。使用贪心算法，多数情况下，即使不能得到最佳结果，结果也已经很不错了。此外，很难定义什么是最好的 
Sudoku 解法，我们也没必要找出最佳解法。这是件好事。创建 Sudoku 游戏可归入一类叫作 <A id=ctl00_MTCS_main_ctl07 
onclick="javascript:Track('ctl00_MTCS_main_ctl00|ctl00_MTCS_main_ctl07',this);" 
href="http://go.microsoft.com/fwlink/?LinkId=63626">NP-complete</A> 
的问题。在外行人眼中，这意味着生成一个最佳 Sudoku 填数游戏需要花费相当长的时间。而事实上，提供“好”解法的启示更受欢迎。</P>
<P>关于 <B>Solver</B>，我已经做了几个方面的解释。首先，您可能已经注意到，在游戏的配置选项和 <B>GeneratorOptions</B> 
类中，生成器可以生成 180 度对称的填数游戏。也就是说，只有 [10-x,10-y] 位置的单元格填充有数字时，才能向 [x,y] 
位置的单元格填入数字（例如，如果第一列、第二行的单元格中有数字，那么第九列、第八行的单元格也应该有数字，反之亦然）。只需对之前所示代码稍加补充，即可完成这步操作。首先，如果使用了对称法则，则应修改 
<B>GetRandomCellOrdering</B> 
方法返回的顺序，以创建对称单元格的随机顺序，而不是单个单元格的随机顺序。这样，对称的单元格在顺序中就可以总是处于相邻位置。这就要求代码可以成对而不是单个地删除值。并且，如果删除其中某一个而导致游戏无效时，这些值可以成对恢复。</P>
<P>生成器还支持不同的难度级别。难度级别基于不同的配置选项，包括必须填充的单元格最小数目，以及玩家要完成游戏需要使用的技巧。这些技巧可以使我们返回 
<B>Solver</B> 和 <B>SolverOptions.EliminationTechniques</B> 属性。</P>
<P>如果您阅读过 Sudoku 
相关的书籍或上网浏览过相关的信息，您会发现，对于如何完成填数游戏，已经有了许多成套的技巧。我曾经讲过这样一个技巧：如果某个单元格的备选数字已经出现在相同行、列或框中的另一个单元格中，则将其删除。如果您浏览下载的源代码的“技巧”文件夹，您会发现我已经实现的几个技巧，例如 
<I>naked subset</I>、<I>hidden subset</I> 和 
<I>x-wing</I>。这些技巧可用来设置网格内单元格中的数字，并且可以从各单元格中删除可能的备选数字。依据 <B>Generator</B> 
的配置难度级别，可以允许 <B>Solver</B> 仅使用特定的技巧，因为某些技巧需要更高的技能。</P>
<P><B>Generator</B> 生成游戏的数量也会影响难度级别。对于特定的难度级别，<B>Generator</B> 
会生成多个而不是单个填数游戏，然后从中选择一个作为最难的。<B>Generator</B> 依据 <B>Solver</B> 
返回的统计数据，如使用哪个技巧完成游戏，以及每种技巧使用多少次等来作出决定。</P>
<P>就像您在源文件中看到的，在目前的实现程序中，共有三个难度级别。 </P>
<UL>
  <LI>
  <P><B>低</B> <B>级</B>，生成三个填数游戏，留有至少 32 个单元格需要填充，只需要一种技巧即可完成整个填数游戏。 </P>
  <LI>
  <P><B>中</B> <B>级</B>，生成 10 个填数游戏，没有必须填充单元格的最小数量限制，可能需要几种技巧。 </P>
  <LI>
  <P><B>高</B> <B>级</B>，生成 20 个填数游戏，没有必须填充单元格的最小数量限制，可能需要所有实现技巧。事实上，难度级别会为求解器打开 
  <B>AllowBruteForce</B> 选项。这意味着，对于生成的填数游戏，只使用提供的一种技巧根本无法完成，也许还会用到“试验 - 
  错误”的逻辑循环。如果您不喜欢这种方式，您完全可以像一些痴迷的 Sudoku 玩家那样，自己修改代码。 </P></LI></UL>
<DIV><A 
href="http://msdn.microsoft.com/zh-cn/library/aa480686.aspx#mainSection"><IMG 
alt="" 
src="Microsoft Sudoku：优化 UMPC 应用程序的点触和手写操作.files/Aa480686.arrow_px_up(zh-cn,MSDN.10).gif" 
border=0> </A><A 
href="http://msdn.microsoft.com/zh-cn/library/aa480686.aspx#mainSection">返回页首</A> 
<BR></DIV>
<H3 id=E0QB0AA>Windows 窗体和硬件交互</H3>
<P>以下章节讨论了 Sudoku 游戏有关 Tablet PC 功能和用户体验的内容。</P>
<P>Sudoku 布局</P>
<P>对于在 Tablet PC 上运行的所有应用程序，一个重要方面是该应用程序的最终显示情况。在不同尺寸的 Tablet PC 
屏幕上，应用程序的显示效果如何？在不同的屏幕方向能否获得很好的图像？能否很好的适应高 DPI 
分辨率和大字体？所有这些因素对应用程序的成功至关重要。当然，我们需要先讨论应用程序结构，否则会难以展开对这些方面的讨论。</P>
<P>我将 Sudoku 设计为 Windows Forms 应用程序，除供用户配置游戏的 <B>OptionsDialog</B> 
外，整个应用程序显示在一个名为 <B>MainForm</B> 的窗体中，位于 MainForm.cs 文件中。<B>MainForm</B> 的 
<B>Controls</B> 集合中仅有的一个控件是 
<B>ImagePanel</B>，该控件固定填充窗体，用作整个应用程序的背景以及窗体下级控件的容器。<B>ImagePanel</B> 
是一个简单的控件，创建它的唯一目的是绘制一个填充控件的图像。</P><PRE>internal class ImagePanel :NoFlickerPanel
{
public ImagePanel(){}

[DesignerSerializationVisibility(
DesignerSerializationVisibility.Hidden)]
[Browsable(false)]
public Image Image { get { return _img; } set { _img = value; } }
private Image _img;

protected override void OnPaint(PaintEventArgs e)
  {
if (_img != null)  
    {
e.Graphics.DrawImage(_img, 0, 0, Width, Height);
    }
base.OnPaint(e);
  }
}
</PRE>
<P><B>ImagePanel</B> 派生自 <B>NoFlickerPanel</B> 控件，而非直接来自 
System.Windows.Forms.Panel。</P><PRE>internal class NoFlickerPanel :Panel
{
public NoFlickerPanel()
  {
SetStyle(ControlStyles.DoubleBuffer |  
ControlStyles.AllPaintingInWmPaint |  
ControlStyles.UserPaint | ControlStyles.ResizeRedraw |  
ControlStyles.SupportsTransparentBackColor, true);
  }
}
</PRE>
<P><B>NoFlickerPanel</B> 用作专门的 <B>Panel</B> 
派生类的基类。除在调整控件大小时自动重绘外，其真正用途是启用双缓冲处理。双缓冲处理可在重绘时防止 <B>Panel</B> 
闪烁（因此得名），对于应用程序的外观至关重要。如果在调整窗体大小时启用自动重绘功能，则当用户更改窗体大小或 Tablet PC 
的屏幕方向时，可以轻松支持重新绘制窗体的操作。</P>
<P>背景面板显示其他四个面板： </P>
<UL>
  <LI>
  <P>一个 <B>NoFlickerPanel</B>，用作网格控件（该控件实际上呈现了游戏并允许用户交互）的容器。 </P>
  <LI>
  <P>两个 <B>ImagePanels</B>，其中具有“屏幕”的控件，用来开始新游戏或继续上次的游戏。 </P>
  <LI>
  <P>一个 <B>ScalingPanel</B>，派生自 
  <B>NoFlickerPanel</B>，处理用于和游戏交互的控件（例如画笔按钮、数字按钮和退出按钮）的整个布局。 </P></LI></UL>
<P>我想在 Sudoku 
中实现真正愉悦的使用体验，能让玩家情不自禁地赞叹：“哇，真棒！”为实现这个目标，需要确保在调整主窗体大小时，窗体上的控件也能相应移动并能调整大小，同时仍保持相对大小和位置。<B>ScalingPanel</B> 
可以实现这一点。</P><PRE>internal class ScalingPanel :NoFlickerPanel
{
public ScalingPanel(){}

private Rectangle _initialBounds;
private Hashtable _controlBounds;
private bool _initialized;

public void ConfigureByContainedControls()
  {
_initialBounds = Bounds;
_controlBounds = new Hashtable();
foreach(Control c in this.Controls) 
    {
_controlBounds.Add(c, c.Bounds);
    }
_initialized = true;
  }

protected override void OnLayout(LayoutEventArgs levent)
  {
if (_initialized &amp;&amp; Width &gt; 0 &amp;&amp; Height &gt; 0 &amp;&amp;  
levent.AffectedControl == this)
    {
//保持图像的原始长宽比
int newWidth = Width;
int tmp = (int)(_initialBounds.Width /  
(double)_initialBounds.Height * Height);
if (tmp &lt; newWidth) newWidth = tmp;

int newHeight = Height;
tmp = (int)(_initialBounds.Height /  
(double)_initialBounds.Width * newWidth);
if (tmp &lt; newHeight) newHeight = tmp;

//记录最大和最短边界
int minX=int.MaxValue, minY=int.MaxValue, maxX=-1, maxY=-1;

//移动并调整所有控件的大小
foreach(Control c in this.Controls) 
      {
Rectangle rect = (Rectangle)_controlBounds[c];

//确定对原始尺寸的最佳猜想
int x = (int)(rect.X / (double)
_initialBounds.Width * newWidth);
int y = (int)(rect.Y / (double)
_initialBounds.Height * newHeight);
int width = (int)(rect.Width / (double)
_initialBounds.Width * newWidth);
int height = (int)(rect.Height / (double)
_initialBounds.Height * newHeight);

//设定新边界
Rectangle newBounds = new Rectangle(
x, y, width, height);
if (newBounds != c.Bounds) c.Bounds = newBounds;

//记录最大和最短边界
if (c.Left &lt; minX) minX = c.Left;
if (c.Top &lt; minY) minY = c.Top;
if (c.Right &gt; maxX) maxX = c.Right;
if (c.Bottom &gt; maxY) maxY = c.Bottom;
      }

//将所有控件居中
int moveX = (Width - (maxX - minX + 1)) / 2;
int moveY = (Height - (maxY - minY + 1)) / 2;

if (moveX &gt; 0 || moveY &gt; 0)
      {
foreach(Control c in this.Controls)  
        {
c.Location = c.Location +  
new Size(moveX - minX, moveY - minY);
        }
      }
    }

//进行基本布局
base.OnLayout(levent);
  }
}
</PRE>
<P><B>ScalingPanel</B> 工作的前提是控件（在 Visual Studio 
的窗体设计器中以可视化的方式创建）的原始相对大小和位置是需要在整个应用程序生命周期内维持的相对大小和位置。在将所有子控件添加到 
<B>ScalingPanel</B> 实例后，客户端将调用<B> ScalingPanel</B> 控件的 
<B>ConfigureByContainedControls</B> 
方法。该方法会初始化将在随后窗体设计中使用的几个状态。首先存储当前的控件边界。然后遍历所有子控件，将各子控件的当前边界存入由 <B>Control</B> 
索引的 System.Collections.Hashtable。掌握了 <B>ScalingPanel</B> 
大小以及各个控件的起始大小和位置之后，<B>ScalingPanel</B> 
就了解了各个控件大小与所在面板大小的比例，以及各个控件之间的相对间隔。更改面板大小时，<B>ScalingPanel</B> 
使用该信息移动位置并更改每个所包含控件的大小，以维持这些大小和位置比例。</P>
<P>所有奇妙的事情都发生在 <B>OnLayout</B> 覆盖中。首先，<B>ScalingPanel</B> 将检查先前是否调用过 
<B>ConfigureByContainedControls</B>，以避免在未调用的情况下进行自定义布局操作。还将检查以确保面板的尺寸大于 
0，以避免在窗体最小化和执行类似会导致控件大小显著缩减的操作时进行布局。最后，<B>ScalingPanel</B> 
将确保布局操作的目标控件是面板本身，以避免当多个与面板相关的控件需要进行布局时执行不必要的重复布局操作。</P>
<P>布局操作包括三个主要步骤。 </P>
<UL>
  <LI>
  <P>确保所有控件边框的图像长宽比保持不变，以便使用该方法，根据面板的当前长和宽，相对于原始长和宽计算该边框新的长和宽。 </P>
  <LI>
  <P>遍历所有控件，根据原始边框与新边框的比例，确定各个控件的位置和大小。 </P>
  <LI>
  <P>使控件框居于面板中心。从而确保了在面板的长宽比与原始长宽比显著不同时，控件仍然居于面板中心。 </P></LI></UL>
<P><B>ScalingPanel</B> 的作用远不止在用户调整窗体时维持漂亮的外观。所有计算都是基于相对大小和位置进行的，这样就获得了免费的高 DPI 
分辨率支持（通常应用于 Tablet PC 上）！对于许多应用程序而言，缺少对高 DPI 分辨率的支持是一个常见缺陷，因为许多开发和测试人员通常不会计划在 
Tablet PC 上运行程序，也没有意识到 Windows 中的 DPI 设置可以更改。加入诸如 <B>ScalingPanel</B> 
一类的控件可以优化您的开发和测试生命周期。</P>
<P>当然，只有当调整面板本身，并且以相对于窗体当前大小的方式进行调整时，<B>ScalingPanel</B> 
中的所有这些功能才会起作用。如前所述，Sudoku 
中的主窗体具有四个子面板控件，但只有两个控件停靠在面板中。控件面板停靠在窗体右侧，停靠的另一个具有网格控件的面板则填充窗体的其余部分。从而在调整主窗体大小时，可以轻松维持两个面板的相对大小。我确保控件面板始终占据窗体宽度的三分之一，允许网格面板占据窗体的剩余部分。</P><PRE>protected override void OnLayout(LayoutEventArgs levent)
{
pnlControls.Width = Width / 3;
base.OnLayout(levent);
}
</PRE>
<P>要了解上述程序如何影响游戏面板，请查看图 5 中的屏幕快照。缩减主窗体的宽度将导致图 6 中的布局，而扩展宽度将导致图 7 中的布局。</P>
<P><IMG alt="" 
src="Microsoft Sudoku：优化 UMPC 应用程序的点触和手写操作.files/Aa480686.tbconsudokusamplefigure05(zh-cn,MSDN.10).gif"> 
</P>
<DIV><B>图 5. 默认布局</B> </DIV>
<P><IMG alt="" 
src="Microsoft Sudoku：优化 UMPC 应用程序的点触和手写操作.files/Aa480686.tbconsudokusamplefigure06(zh-cn,MSDN.10).gif"> 
</P>
<DIV><B>图 6. 缩减宽度后的布局</B> </DIV>
<P><IMG alt="" 
src="Microsoft Sudoku：优化 UMPC 应用程序的点触和手写操作.files/Aa480686.tbconsudokusamplefigure07(zh-cn,MSDN.10).gif"> 
</P>
<DIV><B>图 7. 增加宽度后的布局</B> </DIV>
<P>该方法还可以轻松适应用户的左右手使用习惯。在启动应用程序以及用户随时更改 Windows 左右手使用习惯设置时，都会调用 
<B>SetHandedness</B> 方法。</P><PRE>private void SetHandedness()
{
DockStyle targetStyle = PlatformDetection.UserIsRightHanded ? 
DockStyle.Right :DockStyle.Left;
if (targetStyle != pnlControls.Dock)
  {
pnlControls.SendToBack();
pnlControls.Dock = targetStyle;
  }
}
</PRE>
<P>这是一种非常简单且极为有效的方法。仅使用几行代码，就可以检查用户当前的左右手使用习惯设置，并在当控件面板停靠到主窗体的右侧或左侧时进行相应的更改。这样就可以了！上述布局支持将负责其余部分，且游戏会对用户的喜好更改做出即时响应（我会在电源问题部分调用该方法时进行讨论）。 
</P>
<P>图 8 和图 9 将说明由于左右手使用习惯设置更改而导致的窗体更改。</P>
<P><IMG alt="" 
src="Microsoft Sudoku：优化 UMPC 应用程序的点触和手写操作.files/Aa480686.tbconsudokusamplefigure08(zh-cn,MSDN.10).gif"> 
</P>
<DIV><B>图 8. 右手使用习惯布局</B> </DIV>
<P><IMG alt="" 
src="Microsoft Sudoku：优化 UMPC 应用程序的点触和手写操作.files/Aa480686.tbconsudokusamplefigure09(zh-cn,MSDN.10).gif"> 
</P>
<DIV><B>图 9. 左手使用习惯布局</B> </DIV>
<P>我已说明如何调整控件面板中的控件大小，以及如何相对彼此大小来调整窗体上的两个主面板。其余的自动布局支持由<B> MainForm</B> 中的 
<B>OnResize</B> 覆盖处理。</P><PRE>protected override void OnResize(EventArgs e)
{
//进行基本调整
base.OnResize(e);

//将新游戏和保存的游戏面板居中放置
pnlNewPuzzle.Location = new Point(
backgroundPanel.Location.X +  
(backgroundPanel.Width - pnlNewPuzzle.Width) / 2,  
backgroundPanel.Location.Y +  
(backgroundPanel.Height - pnlNewPuzzle.Height) / 2);
pnlSavedOrNewPuzzle.Location = new Point(
backgroundPanel.Location.X +  
(backgroundPanel.Width - pnlSavedOrNewPuzzle.Width) / 2,  
backgroundPanel.Location.Y +  
(backgroundPanel.Height - pnlSavedOrNewPuzzle.Height) / 2);

//确保在我调整窗体大小时，游戏网格位于中心且为正方形，
//并且尺寸尽可能放大，只要
//不超出其父边界
int width = pnlGrid.Width;
int height = pnlGrid.Height;
int margin = 25;
Size gridSize = width &gt; height ? 
new Size(height - margin, height - margin) : 
new Size(width - margin, width - margin);
thePuzzleGrid.Bounds = new Rectangle(
new Point((width - gridSize.Width)/2,  
(height - gridSize.Height)/2),
gridSize);

//确保无论窗体如何调整，
//字幕进度条都在窗体中心结束。
marqueeBar.Location = new Point(
backgroundPanel.Location.X +  
(backgroundPanel.Width - marqueeBar.Width) / 2,  
backgroundPanel.Location.Y +  
(backgroundPanel.Height - marqueeBar.Height) / 2);
}
</PRE>
<P>该方法用于执行一些简单的布局操作。首先，它使 <I>pnlNewPuzzle</I> 和 <I>pnlSavedOrNewPuzzle</I> 
面板在窗体中居中（多数时间这些面板是隐藏的，只有在启动或用户选择开始新游戏时才显示）。还可在网格面板中调整游戏网格控件的大小和位置（可证明，通过将网格面板设为 
<B>ScalingPanel</B> 可以轻松完成该操作）。最后，还可确保用于显示游戏创建进度的进度条位于窗体正中。</P>
<P>设计进度条是一个有趣的挑战。Windows Forms 1.1 可以显示进度条，但是为“开箱即用”，并且不支持字幕样式。但 Microsoft 
Windows XP 支持。（字幕样式用于显示没有预定义开始和结束点的任务。Windows 启动时显示的进度条为字幕进度条。）Windows Forms 
ProgressBar 控件是 Windows XP 中通用控件进度条的包装，但是没有提供字幕设置（Windows Forms 2.0 
提供）。因此，我创建了用于使用字幕样式的自己的包装，而没有使用 Windows Forms <B>ProgressBar</B>，如图 10 
所示（当然，当您升级至 Windows Vista 后，进度条会变得更漂亮，如图 11 所示）。</P><PRE>internal sealed class MarqueeProgressBar :Control
{
public MarqueeProgressBar()
  {
SetStyle(ControlStyles.SupportsTransparentBackColor, true);
SetStyle(ControlStyles.Selectable |  
ControlStyles.UserPaint, false);
BackColor = Color.Transparent;
ForeColor = SystemColors.Highlight;
  }

protected override CreateParams CreateParams
  {
get
    {
CreateParams cp = base.CreateParams;
cp.ClassName = "msctls_progress32";  
if (!DesignMode) cp.Style |= PBS_MARQUEE;
if (RightToLeft == RightToLeft.Yes)
      {
cp.ExStyle |= WS_EX_LAYOUTRTL;
cp.ExStyle &amp;= ~(WS_EX_RIGHT | WS_EX_RTLREADING |  
WS_EX_LEFTSCROLLBAR);
      }
return cp;
    }
  }

protected override void OnForeColorChanged(EventArgs e)
  {
base.OnForeColorChanged(e);
if (base.IsHandleCreated)
    {
SendMessage(PBM_SETBARCOLOR, 0,  
ColorTranslator.ToWin32(this.ForeColor));
    }
  }

protected override void OnBackColorChanged(EventArgs e)
  {
base.OnBackColorChanged(e);
if (base.IsHandleCreated)
    {
SendMessage(PBM_SETBKCOLOR, 0,  
ColorTranslator.ToWin32(this.BackColor));
    }
  }

protected override ImeMode DefaultImeMode  
  { 
get { return ImeMode.Disable; }  
  }

protected override Size DefaultSize  
  { 
get { return new Size(100, 23); }  
  }

protected override void CreateHandle()
  {
if (!RecreatingHandle)
    {
NativeMethods.INITCOMMONCONTROLSEX icc =  
new NativeMethods.INITCOMMONCONTROLSEX();
icc.dwSize = 0;
icc.dwICC = ICC_PROGRESS_CLASS;
NativeMethods.InitCommonControlsEx(icc);
    }
base.CreateHandle();
  }

private IntPtr SendMessage(int msg, int wparam, int lparam)
  {
return NativeMethods.SendMessage(new HandleRef(this,  
this.Handle), msg, new IntPtr(wparam), new IntPtr(lparam));
  }

protected override void OnHandleCreated(EventArgs e)
  {
base.OnHandleCreated(e);
SendMessage(PBM_SETRANGE, MINIMUM, MAXIMUM);
SendMessage(PBM_SETSTEP, STEP, 0);
SendMessage(PBM_SETPOS, VALUE, 0);
SendMessage(PBM_SETBKCOLOR, 0,  
ColorTranslator.ToWin32(BackColor));
SendMessage(PBM_SETBARCOLOR, 0,  
ColorTranslator.ToWin32(ForeColor));
Start(DEFAULTSPEED);
  }

private void Start(int speed)
  {
if (!DesignMode &amp;&amp; IsHandleCreated)  
SendMessage(PBM_SETMARQUEE, speed == 0 ?0 : 1, speed);
  }

... //常量定义  

}
</PRE>
<P><IMG alt="" 
src="Microsoft Sudoku：优化 UMPC 应用程序的点触和手写操作.files/Aa480686.tbconsudokusamplefigure10(zh-cn,MSDN.10).gif"> 
</P>
<DIV><B>图 10. Windows XP 中的字幕进度条</B> </DIV>
<P><IMG alt="" 
src="Microsoft Sudoku：优化 UMPC 应用程序的点触和手写操作.files/Aa480686.tbconsudokusamplefigure11(zh-cn,MSDN.10).gif"> 
</P>
<DIV><B>图 11. Windows Vista 中的字幕进度条</B> </DIV>
<DIV><A 
href="http://msdn.microsoft.com/zh-cn/library/aa480686.aspx#mainSection"><IMG 
alt="" 
src="Microsoft Sudoku：优化 UMPC 应用程序的点触和手写操作.files/Aa480686.arrow_px_up(zh-cn,MSDN.10).gif" 
border=0> </A><A 
href="http://msdn.microsoft.com/zh-cn/library/aa480686.aspx#mainSection">返回页首</A> 
<BR></DIV>
<H3 id=E0NB0AA>绘制 Sudoku</H3>
<P><B>PuzzleGrid</B> 控件位于 Controls 文件夹的 PuzzleGrid.cs 文件中，是应用程序中最为重要的控件。该控件可显示 
Sudoku 游戏，提供玩 Sudoku 游戏的核心鼠标和键盘支持，以及启用与游戏的笔式交互。</P>
<P>作为一种推荐的做法，控件的所有绘制都应响应 WM_PAINT 消息。在 Windows Forms 中，这通常意味着为控件的 <B>Paint</B> 
事件添加事件处理器或覆盖 <B>OnPaint</B> 方法。我选择后者。</P><PRE>protected override void OnPaint(PaintEventArgs e)
{
base.OnPaint(e);
e.Graphics.PixelOffsetMode = PixelOffsetMode.HighQuality;
DrawToGraphics(e.Graphics, e.ClipRectangle);
}
</PRE>
<P>绘制逻辑可分解为一个单独的 <B>DrawToGraphics</B> 方法和一个矩形，前者可接受目标 <B>Graphics</B> 
对象，后者则可以定义控件中无效的边界。（只重新绘制绝对需要绘制的图形可极大提高应用程序的性能；在我最初增加实现该功能时，性能的提高即刻显现）。当您以这种方式分解绘制代码时，打印支持会得到显著的简化，同时将控件呈现给 
<B>Bitmap</B> 对象的能力也有所提高。这一点在胜利动画中有所展现，其中我使用该功能显示玩家填数成功时的祝贺动画。</P>
<P>代码不是很长，以简洁明了的方式绘制游戏盘。从绘制背景图像开始，这些图像位于代码下载的 Images 文件夹中。</P><PRE>graphics.DrawImage(ResourceHelper.BoardBackgroundImage,  
0, 0, Width, Height);
graphics.DrawImage(ResourceHelper.BoardImage, rect);
</PRE>
<P>绘制第一个图像来填充控件。第二个图像根据私有 <B>BoardRectangle</B> 
属性返回的矩形绘制，此矩形比控件略小，允许在边界周围存在一些较窄的空白边距。私有的 <B>GetCellRectangle</B> 
方法除考虑背景图像相对于整个图像的间隙大小和位置外，还考虑背景图像上各单元格图像相对于整个图像的已知大小，因此可用于确定各单元格的大小和位置。我使用 
<B>GetCellRectangle</B> 返回的大小来确定某个单元格是否需要进一步检查和绘制。</P><PRE>for (int i = 0; i &lt; State.GridSize; i++)
{
for (int j = 0; j &lt; State.GridSize; j++)
  {
RectangleF cellRect = GetCellRectangle(rect, new Point(i,j));
if (clipRectangle.IntersectsWith(Rectangle.Ceiling(cellRect)))
    {
... //在此绘制单元格
    }
  }
}
</PRE>
<P>类似地，我使用由 <B>OnPaint</B> 向 <B>DrawToGraphics</B> 提供的 <B>clipRectangle</B> 
信息，避免呈现没有失效的单元格。因为绘制是比较耗用 CPU 
资源的操作，并且通常只有控件的较小区域发生失效（例如向单元格输入数字或选定的单元格发生更改时），因此可显著提高控件的可用性。</P>
<P>当我决定绘制一个单元格时，应该进行如下操作。首先，假设填数没有成功，检查单元格当前是否选定。如果单元格被选定，使用五个绿色图像（位于 Images 
文件夹中）中的一个绘制单元格。不管选定的单元格是否是四角中的一个，这五个图像都与之对应。</P><PRE>Image selectedCellImage;
if (i == 0 &amp;&amp; j == 0) 
selectedCellImage = ResourceHelper.CellActiveUpperLeft;
else if (i == 0 &amp;&amp; j == State.GridSize-1) 
selectedCellImage = ResourceHelper.CellActiveUpperRight;
else if (i == State.GridSize-1 &amp;&amp; j == 0) 
selectedCellImage = ResourceHelper.CellActiveLowerLeft;
else if (i == State.GridSize-1 &amp;&amp; j == State.GridSize-1) 
selectedCellImage = ResourceHelper.CellActiveLowerRight;
else 
selectedCellImage = ResourceHelper.CellActiveSquare;
graphics.DrawImage(selectedCellImage, 
cellRect.X, cellRect.Y, cellRect.Width, cellRect.Height);
</PRE>
<P>如果用户选择提供有关下一个要填充的单元格的提示，或者当前呈现的单元格就是建议的单元格，则会使用类似的逻辑在单元格上绘制 <I>suggested cell 
</I>图像。<B>Solver</B> 和 <B>Generator</B> 
使用某种技术确定游戏的难度级别和游戏的可解性，现在使用同一技术来确定建议的单元格。私有 <B>SuggestedCell</B> 
属性根据这些技术计算建议的单元格并缓存计算结果，因此无需在每次绘制网格时重新计算（更改游戏状态时缓存的结果无效）。在内部，<B>SuggestedCell</B> 
可通过如下代码实现。</P><PRE>TechniqueCollection tc = new TechniqueCollection();
FastBitArray [][] possibleNumbers = 
PuzzleState.InstantiatePossibleNumbersArray(State);
foreach(EliminationTechnique et in 
EliminationTechnique.AvailableTechniques)
{
tc.Add(et);
Hashtable ignored = null;
State.ComputePossibleNumbers(tc, ref ignored, true, 
true, possibleNumbers);
for(int row=0; row&lt;State.GridSize; row++)
  {
for(int column=0; column&lt;State.GridSize; column++)
    {
if (possibleNumbers[row][column].CountSet == 1)
      {
return _suggestedCell = new Point(row, column);
      }
    }
  }
}
</PRE>
<P>该逻辑以从最简单到最复杂的顺序检索所有可用的技术（该顺序根据我对技术难度级别的评估大致确定）。从最简单的技术开始，确定通过使用该技术，网格中是否存在只拥有一个可填充的备选数字的单元格。如果存在此类空单元格，则第一个查找到的单元格被返回为建议的单元格。如果没有找到，则下一个技术将被添加到组合中，重新计算可能的备选数字。如果找到了只拥有一个可能备选数字的多个单元格，则将返回其中的第一个单元格。此过程一直继续进行，直到找到建议的单元格或所有技术用尽仍没有找到建议的单元格为止（只有在游戏生成过程中，允许在难度级别使用强力技术才会发生这种情况）。这种情况下，带有最少的剩余可能备选数字的单元格将被返回为建议的单元格。</P>
<P>单元格背景绘制完成后，如果其中已存在一个输入的数字，则该数字将被呈现。</P><PRE>if (State[i, j].HasValue)
{
Brush b;
if (ShowIncorrectNumbers &amp;&amp;
State[i, j].HasValue &amp;&amp; _solvedOriginalState != null &amp;&amp; 
State[i, j].Value != _solvedOriginalState[i, j].Value)
  {
b = _incorrectValueBrush;
  }
else if (_originalState != null &amp;&amp; _originalState[i,j].HasValue)
  {
b = _originalValueBrush;
  } 
else b = _userValueBrush;

graphics.DrawString((State[i, j] + 1).ToString(
CultureInfo.InvariantCulture), setNumberFont, b,
cellRect, _centerNumberFormat);
}
</PRE>
<P>可从以下三个缓存的画笔中选择一个与 <B>DrawString</B> 一起使用： </P>
<UL>
  <LI>
  <P>红色画笔显示不正确的值。 </P>
  <LI>
  <P>黑色画笔显示初始游戏部分值。 </P>
  <LI>
  <P>蓝色画笔显示输入值。 </P></LI></UL>
<P>这些画笔将被缓存，因此无需在每次绘制游戏时再次创建（另一种性能优化）。如下情况中，将选定显示不正确值的画笔： </P>
<UL>
  <LI>
  <P>用户选择在游戏当中警报错误。 </P>
  <LI>
  <P>单元格中的值与完成的填数游戏的值不匹配，该值在新游戏初始存入时由网格创建。 </P></LI></UL><PRE>public void SetOriginalPuzzleCheckpoint(PuzzleState original)
{
_originalState = original;
if (original != null)
  {
SolverOptions options = new SolverOptions();
options.MaximumSolutionsToFind = 2;
SolverResults results = Solver.Solve(original, options);
if (results.Status == PuzzleStatus.Solved &amp;&amp; 
results.Puzzles.Count == 1)
    {
_solvedOriginalState = results.Puzzle; 
    }
else _solvedOriginalState = null;
  }
}
</PRE>
<P><B>DrawToGraphics</B> 最后要进行的操作是通过启动调用其私有 <B>RenderInk</B> 
方法，呈现任何需要在网格中显示的手写内容。这将我带入了 Sudoku 实现的 pièce de résistance：触控笔支持。</P>
<DIV><A 
href="http://msdn.microsoft.com/zh-cn/library/aa480686.aspx#mainSection"><IMG 
alt="" 
src="Microsoft Sudoku：优化 UMPC 应用程序的点触和手写操作.files/Aa480686.arrow_px_up(zh-cn,MSDN.10).gif" 
border=0> </A><A 
href="http://msdn.microsoft.com/zh-cn/library/aa480686.aspx#mainSection">返回页首</A> 
<BR></DIV>
<H3 id=E0KB0AA>启用 Tablet PC 交互</H3>
<P>为 Tablet PC 
编写的游戏应简单易玩，并且必须支持笔式交互。最低限度上，应用程序应将触控笔用作导航设备。理想情况下，应用程序应支持手写识别，并将其作为数据输入的方法之一 - 
尽管对该功能的支持程度不尽相同。同样，启用笔式交互所涉及的工作量也有所不同。应用程序也可以利用手势支持功能，将特定的触控笔移动解释为操作，而不是解释为要识别的手写内容。对于在带有触摸屏的 
Tablet PC（例如 ultra-mobile PC）上使用的应用程序而言，在设计和开发阶段还应考虑点触支持。我的 Sudoku 
实现遵循了所有这些核心原则。</P>
<P>对于 Tablet PC 上的 Sudoku 
游戏，核心要求是您可以将数字写入单元格。这会使游戏具有在纸上书写的感觉。手写的数字随后被转换为可呈现的类型，而非保留为书写内容以用于实现；无论如何，应用程序具有识别输入的能力非常重要。我选择识别每个单独的输入，以打印字体呈现所有手写数字，而不是以原始的手写体呈现。</P>
<P>所有笔式交互均由 <B>PuzzleGrid</B> 控件处理。<B>PuzzleGrid</B> 具有两种类型的私有成员，来自 
Microsoft.Ink.dll 程序集。</P><PRE>private InkOverlay _inkOverlay;
private RecognizerContext _recognizerCtx;
</PRE>
<P>InkOverlay 类用作所有触控笔交互处理的基础。<B>InkOverlay</B> 附加在任何现有的 <B>Control</B> 
上，允许与该控件进行笔式交互。RecognizerContext 类提供了用于识别用户输入数字的手写识别基础。 </P>
<P><B>PuzzleGrid</B> 中的 <B>EnableTabletSupport</B> 方法将初始化 <B>InkOverlay</B> 和 
<B>RecognizerContext</B>。首先，我通过使用先前讨论的逻辑，检索要使用的来自 <B>PlatformDetection</B> 
类的默认识别程序。然后使用返回的 <B>Recognizer</B> 配置 <B>RecognizerContext</B>。</P><PRE>Recognizer defaultRecognizer =
PlatformDetection.GetDefaultRecognizer();
_recognizerCtx = defaultRecognizer.CreateRecognizerContext();
_recognizerCtx.Factoid = Factoid.Digit;
</PRE>
<P><B>RecognizerContext</B> 提供了 Factoid 属性，您可以通过它就要求识别的输入类型向 
<B>RecognizerContext</B> 给出提示。通过 factoid 
属性对识别程序进行恰当的分类之后，显著提高了用户与应用程序交互的质量，使识别程序转向指定输入的分类。对于 Sudoku，只要求识别程序识别输入的数字 1 到 
9。通过将 <B>Factoid</B> 属性设为 Factoid.Digit，使识别程序侧重于对此类输入的识别。<B>Factoid.Digit</B> 
将识别程序的侧重点设为单个数字，以便准备并趋于只识别单个数值（请注意，只有少数几个 factoid 
可保证与识别程序在所有语言识别中一起使用；幸运的是，<B>Digit</B> 是其中一个）。</P>
<P>配置 <B>RecognizerContext</B> 之后，我初始化了 <B>InkOverlay</B>。</P><PRE>_inkOverlay = new InkOverlay(this, true);
_inkOverlay.Ink.CustomStrokes.Add(ScratchpadStrokesID,  
_inkOverlay.Ink.CreateStrokes());
_inkOverlay.Ink.CustomStrokes.Add(NormalStrokesID,  
_inkOverlay.Ink.CreateStrokes());
_inkOverlay.DefaultDrawingAttributes.Color = _inkColor; 
</PRE>
<P><B>InkOverlay</B> 将被实例化并绑定到 <B>PuzzleGrid</B>（this 引用，因为此代码存在于 
<B>PuzzleGrid</B> 类中）。我将两个 CustomStrokes 集合添加到 
<B>InkOverlay</B>。稍候我会在本文中对此进行说明，但是简而言之，它们使得区分用户输入的不同类型的手写内容更为简单，还可以轻松地永久保存所保存游戏的手写内容。然后配置覆盖图手写内容的颜色并设置覆盖图的 
CollectionMode。 </P>
<P><B>CollectionMode</B> 
属性会告诉覆盖图是否应将输入当作手写内容、手势（触控笔的移动被解释为特定操作，而非内容）或两者。如果当前没有手势识别程序，则尝试为手势集合启用 
<B>InkOverlay</B> 将产生错误，因此我再次使用 <B>PlatformDetection</B> 
类确定当前是否存在可用的手势识别程序。如果存在，将设置 <B>CollectionMode.InkAndGesture</B>；否则将设置 
<B>CollectionMode.InkOnly</B>。</P><PRE>bool gestureRecognizerInstalled =  
PlatformDetection.GestureRecognizerInstalled;
_inkOverlay.CollectionMode = gestureRecognizerInstalled ? 
CollectionMode.InkAndGesture :CollectionMode.InkOnly;
</PRE>
<P>如果设置了 <B>CollectionMode</B>，将告诉 <B>InkOverlay</B> 
其是否可以接受手势，但是没有专门说明要求的手势，也没有说明当这些手势出现时应进行的操作。对于 Sudoku 而言，我只关注 
ApplicationGesture.Scratchout，用户通过它可以擦除先前在网格中输入的数字，将数字从游戏盘上删除。为了启用对手势的支持，我使用了 
InkOverlay.SetGestureStatus 方法，并使用 InkOverlay.Gesture 
事件注册了一个事件处理器，以便处理擦除手势的接受操作。</P><PRE>if (gestureRecognizerInstalled)
{
_inkOverlay.SetGestureStatus(
ApplicationGesture.AllGestures, false);
_inkOverlay.SetGestureStatus(
ApplicationGesture.Scratchout, true);
_inkOverlay.Gesture +=  
new InkCollectorGestureEventHandler(HandleGesture);
}
</PRE>
<P>然后配置 <B>InkOverlay</B> 类的 AutoRedraw 和 DynamicRendering 属性。如果将 
<B>AutoRedraw</B> 设置为 <B>true</B>，则 <B>InkOverlay</B> 将自动呈现任何捕获到的完整的笔画。如果将 
<B>DyamicRendering</B> 设置为 <B>true</B>，则 <B>InkOverlay</B> 
将自动呈现当前所捕获的任何笔画。为了启用手写内容呈现的双缓冲处理，我选择自己绘制捕获的手写内容，因此将 <B>AutoRedraw</B> 设置为 
<B>false</B>。但是，我更乐意让 <B>InkOverlay</B> 呈现当前用户正在输入的手写内容，因此将 
<B>DynamicRendering</B> 设置为 <B>true</B>。</P><PRE>_inkOverlay.AutoRedraw = false;
_inkOverlay.DynamicRendering = true;
</PRE>
<P><B>InkOverlay</B> 的配置很快就要完成了。需要告诉 <B>InkOverlay</B> 如何响应特定的触控笔交互，可通过 
<B>InkOverlay</B> 上一组事件的注册处理程序来实现。 </P>
<UL>
  <LI>
  <P>当用户完成一个新笔画时，将触发 Stroke 事件。 </P>
  <LI>
  <P>当触控笔进入 tablet PC 触摸屏的物理检测范围（靠近）时，将触发<B> </B>CursorInRange 事件。 </P>
  <LI>
  <P>当 <B>InkOverlay</B> 收到一个新的信息包，其中包含来自正在接触屏幕的触控笔数据，将触发 NewPackets 事件。 </P>
  <LI>
  <P>当 <B>InkOverlay</B> 收到的信息包中包含来自屏幕上方的触控笔空中移动数据时，将触发 NewInAirPackets 事件。 </P>
  <LI>
  <P>当从<B> InkOverlay</B> 类的 <B>Strokes</B> 集合中删除笔画时，将触发 StrokesDeleting 事件。 
  </P></LI></UL>
<P><B>请注意</B> <B>CursorInRange</B> 和 <B>NewInAirPackets</B> 
事件将报告给电磁数字转换器和鼠标，但不会报告给触摸式数字转换器。 </P><PRE>_inkOverlay.Stroke += new InkCollectorStrokeEventHandler(HandleStroke);
_inkOverlay.CursorInRange +=  
new InkCollectorCursorInRangeEventHandler(HandleCursorInRange);
_inkOverlay.NewPackets +=  
new InkCollectorNewPacketsEventHandler(HandleNewPackets);
_inkOverlay.NewInAirPackets +=  
new InkCollectorNewInAirPacketsEventHandler(HandleNewInAirPackets);
_inkOverlay.StrokesDeleting +=  
new InkOverlayStrokesDeletingEventHandler(HandleStrokesDeleting);
</PRE>
<P>最后启用了 <B>InkOverlay</B>。</P><PRE>_inkOverlay.Enabled = true;
</PRE>
<DIV><A 
href="http://msdn.microsoft.com/zh-cn/library/aa480686.aspx#mainSection"><IMG 
alt="" 
src="Microsoft Sudoku：优化 UMPC 应用程序的点触和手写操作.files/Aa480686.arrow_px_up(zh-cn,MSDN.10).gif" 
border=0> </A><A 
href="http://msdn.microsoft.com/zh-cn/library/aa480686.aspx#mainSection">返回页首</A> 
<BR></DIV>
<H3 id=E0HB0AA>识别多个笔画组成的数字</H3>
<P>需要识别用户输入时，我会使用 <B>RecognizeStrokes</B> 方法。</P><PRE>private bool RecognizeStrokes(Strokes strokes, out byte number)
{
number = 0;
if (_recognizerCtx != null &amp;&amp; strokes.Count &gt; 0)
  {
_recognizerCtx.Strokes = strokes;
_recognizerCtx.EndInkInput();

RecognitionStatus rs;
RecognitionResult rr = _recognizerCtx.Recognize(out rs);
if (rr != null &amp;&amp; rs == RecognitionStatus.NoError)
    {
string inputNumberText = rr.TopString;
if (inputNumberText != null &amp;&amp; inputNumberText.Length &gt; 0)
      {
try
        {
number = byte.Parse(inputNumberText, 
CultureInfo.InvariantCulture);
        } 
catch(OverflowException){}
catch(FormatException){}
if (number &gt;= 1 &amp;&amp; number &lt;= 9) return true;
      }
    }
  }
return false;
}
</PRE>
<P><B>RecognizeStrokes</B> 
接受两个参数：要识别的笔画集合以及包含识别数字的输出字节。它具有一个布尔返回值，表示输入是否以可接受的方式识别，即输出的数字是否可以用作输入。首先，<B>RecognizeStrokes</B> 
将检查以确保其具有一个可用的有效 <B>RecognizerContext</B>，并检查是否为其提供了任何笔画。假定两个条件都经过检验，它将笔画存入 
<B>RecognizerContext</B> 并使用 <B>EndInkInput</B> 方法通知 <B>RecognizerContext</B> 
已经完成接受用于识别的输入。然后调用 Recognize 方法进行识别。</P>
<P><B>Recognize</B> 提供了几条信息。从 <B>Recognize</B> 返回的 RecognitionResult 
对象包含关于操作结果的信息，您可以使用所提供的 RecognitionStatus 
枚举值（作为方法输出参数）确定识别过程中是否发生了错误，如果其间发生了错误，则确定错误类型。<B>RecognitionResult</B> 对象的 
TopString 属性将作为已识别文本的字符串返回，此示例中应作为单个数字返回。然后使用 <B>byte.Parse</B> 
方法验证数字是否被真正识别，如果数字被识别，则将其返回到调用程序。</P>
<P>我可以在用户输入笔画时使用 <B>RecognizeStrokes</B>，这正是在最初实现 Sudoku 
时所进行的操作。但是对于任何多笔画书写的数字都会产生问题（这在最初没有料到，因为我用单个连续的笔画书写所有的数字）。收到多笔画输入的某一个笔画后，将调用 
<B>RecognizeStrokes</B>，如果还有大量笔画尚未输入，则大多数情况下都不能正确识别输入。</P>
<P>我在 Sudoku 中使用计时器解决了此问题。当 <B>InkOverlay</B> 接收到 <B>Stroke</B> 时，将启动一个周期为 0.5 
秒的计时器。如果 <B>InkOverlay</B> 在计时器超时前又接收到一个笔画，则重设计时器，允许另一个 0.5 
秒的间隔。最终，计时器会在玩家添加更多笔画前超时。计时器超时后，将调用计时器的事件处理器，从中识别用户输入，并根据玩家输入的笔画位置将其存入游戏相应的单元格中。</P><PRE>if (strokes.Count &gt; 0)
{
byte number = 0;
bool recognized = false;

Point cell = GetCellFromStroke(strokes[0]);
if (CanModifyCell(cell))  
  {
recognized = RecognizeStrokes(strokes, out number);
  }

_inkOverlay.Ink.DeleteStrokes(strokes);
strokes.Clear();

if (recognized) SetStateCell(cell, (byte)(number-1));
}
</PRE>
<P>但是请注意，我并没有以相同方式处理所有笔画。需要特别指出的是，我会忽略我认为错误的笔画，例如触控笔偶然触碰屏幕或用户点击屏幕更改当前选定的单元格而产生的笔画。为此，我将 
<B>Stroke</B> 的边界框矩形从手写内容空间坐标转换为控件空间。</P><PRE>Rectangle boundingBox = e.Stroke.GetBoundingBox();
InkToPixelSpace(ref boundingBox);
</PRE>
<P>然后将边界框的长和宽与网格中单元格的长和宽进行比较。如果边界框的长或宽小于单元格尺寸的某个预定义分数，则忽略 <B>Stroke</B>。通过将事件参数的 
<B>Cancel</B> 属性设置为 <B>true</B> 即可完成，这样 Tablet PC API 
将自动删除笔画。我还需要确保在输入正常笔画但尚未识别之前重新启动多笔画计时器。</P><PRE>RectangleF cellRect = GetCellRectangle(ClientRectangle, Point.Empty);
if (boundingBox.Width &lt;  
cellRect.Width * MINIMINUM_BOUNDING_RATIO_TO_RECOGNIZE &amp;&amp;
boundingBox.Height &lt;  
cellRect.Height * MINIMINUM_BOUNDING_RATIO_TO_RECOGNIZE)
{
e.Cancel = true;
  ...
}
</PRE>
<P>我还使用许多其他事件来识别笔画。假设用户在上一单元格输入数字结束后，会将触控笔移向其他单元格，同时触控笔会触碰到屏幕或悬停在空中。因此，这时我就会停止计时器并自动识别笔画，如同计时器已经超时。(<B>RecognizePreviousCellFromPacket</B> 
将检查以确定当前笔画所在的单元格是否与触控笔下的单元格不同；如果两者位于同一单元格内，则可以编辑这些笔画，这时将调用 
<B>RecognizeStrokes</B>。）</P><PRE>private void HandleNewInAirPackets(
object sender, InkCollectorNewInAirPacketsEventArgs e)
{
if (e.PacketData.Length &gt;= 2)
  {
RecognizePreviousCellFromPacket(
new Point(e.PacketData[0], e.PacketData[1]));  
  }
}
</PRE>
<P>响应 <B>NewPackets</B> 和 <B>Stroke</B> 事件所用的逻辑相同。图 12 显示了通过多个笔画输入的数字（例如数字 4 
由两个笔画组成），图 13 显示了数字被识别并呈现后的结果。</P>
<P><IMG alt=tbconsudokusamplefigure12 
src="Microsoft Sudoku：优化 UMPC 应用程序的点触和手写操作.files/Aa480686.tbconsudokusamplefigure12(zh-cn,MSDN.10).gif"> 
</P>
<DIV><B>图 12. 数字 4 由多个笔画组成</B> </DIV>
<P><IMG alt="" 
src="Microsoft Sudoku：优化 UMPC 应用程序的点触和手写操作.files/Aa480686.tbconsudokusamplefigure13(zh-cn,MSDN.10).gif"> 
</P>
<DIV><B>图 13. 数字 4 由多个笔画识别</B> </DIV>
<DIV><A 
href="http://msdn.microsoft.com/zh-cn/library/aa480686.aspx#mainSection"><IMG 
alt="" 
src="Microsoft Sudoku：优化 UMPC 应用程序的点触和手写操作.files/Aa480686.arrow_px_up(zh-cn,MSDN.10).gif" 
border=0> </A><A 
href="http://msdn.microsoft.com/zh-cn/library/aa480686.aspx#mainSection">返回页首</A> 
<BR></DIV>
<H3 id=E0EB0AA>撤消支持</H3>
<P>多级撤消是所有应用程序中我最喜欢的功能之一，自然也在 Sudoku 应用程序中实现了该功能。通过使用 
<B>PuzzleState.Clone</B>，可以相当容易的实现该功能。</P>
<P>基本理念是每次对游戏盘进行更改时，都会将当前状态的副本压入撤消堆栈中。</P><PRE>private PuzzleStateStack _undoStates = new PuzzleStateStack();
</PRE>
<P><B>PuzzleStateStack</B> 类型是一个小的强类型类，从 <B>System.Collections</B> 中的 Stack 
类派生而来。以后如果要撤消并返回到先前状态，则可从撤消堆栈中弹出状态，并将其设为当前状态即可。</P><PRE>public void Undo()
{
if (_undoStates.Count &gt; 0) this.State = _undoStates.Pop();
}
</PRE>
<P>唯一的技巧就是确保每次更改当前状态时，都要将当前状态压入撤消堆栈。在内部，<B>PuzzleGrid</B> 
可确保在任何时候作出更改时都会将状态压入堆栈，但是 <B>PuzzleGrid</B> 会公开提供当前的 <B>PuzzleState</B> 
实例，因此利用该实现，不能保证每次更改都会被成功记录。因此，<B>PuzzleGrid</B> 通过提供 <B>SetUndoCheckpoint</B> 
成员并赋予控件使用者适当责任，确保对当前状态进行任何重大更改之前都调用 <B>SetUndoCheckpoint</B>。</P><PRE>public void SetUndoCheckpoint()
{
_undoStates.Push(State.Clone());
}
</PRE>
<P><B>PuzzleGrid</B> 还在内部使用 
<B>SetUndoCheckpoint</B>。要想了解何时何地被调用，示例之一就是多笔画计时器事件处理器（如前所述）中使用的 
<B>SetStateCell</B> 方法。</P><PRE>private void SetStateCell(Point cell, byte number)
{
if (State[cell] != number)
  {
SetUndoCheckpoint();
ClearTabletStateCell(cell);
State[cell] = number;
  }
}
</PRE>
<P>该方法首先检查以确保目标单元格的新值与已存在的值不同。假设必须进行更改，该方法将设置一个撤消检查点，将当前的 <B>PuzzleState</B> 
压入撤消堆栈。然后向单元格添加一个值。该方法还要调用 <B>ClearTabletStateCell</B> 
方法。<B>ClearTabletStateCell</B> 在游戏中主要处理便签簿功能。</P>
<DIV><A 
href="http://msdn.microsoft.com/zh-cn/library/aa480686.aspx#mainSection"><IMG 
alt="" 
src="Microsoft Sudoku：优化 UMPC 应用程序的点触和手写操作.files/Aa480686.arrow_px_up(zh-cn,MSDN.10).gif" 
border=0> </A><A 
href="http://msdn.microsoft.com/zh-cn/library/aa480686.aspx#mainSection">返回页首</A> 
<BR></DIV>
<H3 id=E0BB0AA>便签簿支持</H3>
<P>为了让玩家在玩游戏的时候能有在纸上玩的感觉，我在 Sudoku 中实现的主要功能之一就是铅笔便签功能，在代码中称作 
<I>scratchpad</I>。也就是说，您可通过该功能进行记录，能在整个游戏过程中跟踪您的逻辑演绎，类似于在纸上游戏中使用铅笔进行记录（见图 
14）。这些记录不会被识别为触控笔写入的内容（在代码中称作 <I>normal</I>）；而是作为标记数据与 <B>PuzzleState</B> 
一起存储。</P>
<P><IMG alt="" 
src="Microsoft Sudoku：优化 UMPC 应用程序的点触和手写操作.files/Aa480686.tbconsudokusamplefigure14(zh-cn,MSDN.10).gif"> 
</P>
<DIV><B>图 14. 使用便签簿进行记录</B> </DIV>
<P>在说明如何实例化和配置所用的 <B>InkOverlay</B> 时，我曾简单谈起两条指令。</P><PRE>_inkOverlay.Ink.CustomStrokes.Add(ScratchpadStrokesID,  
_inkOverlay.Ink.CreateStrokes());
_inkOverlay.Ink.CustomStrokes.Add(NormalStrokesID,  
_inkOverlay.Ink.CreateStrokes());
</PRE>
<P><B>InkOverlay</B> 类的 <B>Ink</B> 属性将返回当前与覆盖图相关的 <B>Ink</B> 对象，并且其 
<B>CreateStrokes</B> 方法将创建与该 <B>Ink</B> 对象相关的新 <B>Strokes</B> 集合。通过创建和维护两个不同的 
<B>Strokes</B> 
集合，可以轻松的区别在<I>正常</I>触控笔模式和<I>便签簿</I>铅笔模式下创建的笔画。从而可以轻松地针对其中一种类型的笔画进行操作。为了简单的检索集合并确保它们与 
<B>Ink</B> 对象一起被正确的<I>序列化</I>，我将这些集合都添加到<B> Ink</B> 对象的 <B>CustomStrokes</B> 
集合中（<I>序列化</I>是将 <B>Ink</B> 
对象转换成供存储的字节数组的过程。有两个不同的使用目的，我将简要说明）。<B>CustomStrokes</B> 集合中包含若干名为 
<B>Strokes</B> 的集合，这些集合将被保留以供今后使用。我还定义了几个助手属性，可以轻松地查询和使用这些集合。</P><PRE>private Strokes NormalStrokes  
{
get { return _inkOverlay.Ink.CustomStrokes[NormalStrokesID]; }
}
private Strokes ScratchpadStrokes
{
get { return _inkOverlay.Ink.CustomStrokes[ScratchpadStrokesID];}
}
private bool HasScratchpadStrokes  
{ 
get 
  {
using(Strokes strokes = ScratchpadStrokes)
    {
return strokes.Count &gt; 0;  
    }
  }
}
</PRE>
<P>请注意，如果便签簿笔画集合中存在任何笔画，则 <B>HasScratchpadStrokes</B> 将返回 
<B>true</B>，使用完毕之后会对便签簿 <B>Strokes</B> 集合进行处理。这似乎很奇怪，但您在检查 <B>CustomStrokes</B> 
索引生成器实际执行的操作后就会明白。以下是其实现的伪代码。</P><PRE>public Strokes this[string name]
{
get { return new Strokes(this.m_CustomStrokes.Item(name)); }
}
</PRE>
<P>它会返回新的 <B>Strokes</B> 对象，而不是最初存储在集合中的对象。删除的就是这个 <B>Strokes</B> 
对象，而不是原始的那个。当从<B> InkOverlay</B> 上的 <B>Ink</B> 对象存取主 <B>Strokes</B> 
集合时，情况也是如此。以下是 <B>Strokes</B> 对象获取存取器的伪代码。</P><PRE>public Strokes Strokes
{
get
  {
if (this.disposed)
    {
throw new ObjectDisposedException(GetType().FullName);
    }
return new Strokes(this.m_Ink.Strokes);
  }
}
</PRE>
<P>您每次存取 <B>Strokes</B> 属性时都会返回一个新的 <B>Strokes</B> 实例，您需要确保在结束时删除这些对象。例如，以下代码显示了 
<B>PuzzleGrid</B> 用于手动绘制所有手写内容的 <B>RenderInk</B> 方法（请记住我们关闭了 <B>InkOverlay</B> 
上的 <B>AutoRedraw</B>）。</P><PRE>private void RenderInk(Graphics graphics)
{
if (_inkOverlay != null)
  {
using(Strokes strokes = _inkOverlay.Ink.Strokes)
    {
if (strokes.Count &gt; 0)
      {
_inkOverlay.Renderer.Draw(graphics, strokes);
      }
    }
  }
}
</PRE>
<P><B>PuzzleGrid</B> 
记录了网格是否处于便签簿模式；该转换由主窗体上的铅笔和画笔按钮控制。处于便签簿模式时，<B>InkOverlay</B> 上 <B>Stroke</B> 
事件的事件处理器不会执行前面所述的多笔画逻辑。相反，它会向自定义便签簿的 <B>Strokes</B> 集合添加所有检索到的笔画。但还需要一些附加工作。</P>
<P>首先，各个便签簿笔画必须将其自身的更改保存在撤消堆栈中，以便单个记录可以撤消。在将笔画数据添加到 <B>Ink</B> 
对象之前，<B>InkOverlay</B> 上不存在任何触发的事件，因此很难实现这一操作。需要一个小技巧来处理这种情况。必须从 <B>Ink</B> 
对象中删除笔画，设置撤消检查点，然后再重新添加笔画。不幸的是，删除的笔画不再可用。解决方法是将笔画序列化，删除最初的笔画，然后在设置撤消检查点后创建一个全新的笔画（基于来自旧笔画的数据）。然后就可以将新笔画添加至 
<B>Ink</B> 对象。</P><PRE>Stroke s = e.Stroke;
TabletPropertyDescriptionCollection tpdc =  
new TabletPropertyDescriptionCollection();
foreach (Guid g in s.PacketDescription)
{
TabletPropertyDescription tpd = new TabletPropertyDescription(
g, e.Cursor.Tablet.GetPropertyMetrics(g));
tpdc.Add(tpd);
}
int[] packetData = e.Stroke.GetPacketData();
_inkOverlay.Ink.DeleteStroke(e.Stroke);
SetUndoCheckpoint();
s = _inkOverlay.Ink.CreateStroke(packetData, tpdc);
</PRE>
<P><B>Stroke</B> 由一系列点组成，每个点都是由数据包产生的。您可以在 <B>Stroke</B> 上使用 GetPacketData 
方法检索该信息包数据的数组，并且 <B>Ink</B> 对象中的 <B>CreateStroke</B> 方法有一个重载，可用于接受此类数组并从中创建一个新的 
<B>Stroke</B>。问题是，该方法还需要信息包数据中所包含内容的描述，并且该描述应该以 
TabletPropertyDescriptionCollection 的形式给出。因此，在删除 <B>Stroke</B> 
之前，您需要检查其是否具有该描述。<B>Stroke</B> 中的 PacketDescription 属性将返回一个 Guids 数组，针对 
<B>Stroke</B> 中存储的每个信息包数据类型均返回一个数组。可以枚举这些数组，以建立 <B>CreateStroke</B> 调用所需的 
<B>TabletPropertyDescriptionCollection</B>。</P>
<P>利用现有的撤消支持，可让便签簿笔画显示不同的颜色，以将便签簿笔画与正常笔画区别开来。</P><PRE>s.DrawingAttributes.Color = _scratchpadInkColor;
</PRE>
<P>还有一小段技巧性的代码。在本文开始，我详细说明了支持可调整窗体大小的重要性，并且说明了应该如何调整不同控件的大小，例如 
<B>PuzzleGrid</B>。每个笔画对象也需要调整大小和位置，以便保持相对于 <B>PuzzleGrid</B> 
的大小和位置。说起来容易做起来难。我的解决方案是必须使每个便签簿笔画在创建时与笔画和网格的当前大小相关联。每次调整网格大小时，这一信息使得笔画大小也能得到相应调整。根据笔画的原始大小和位置进行转换，而不是其当前的大小和位置，从而避免了舍入误差（与以浮点数存储该数据相关）所产生的问题；如果频繁调整大小，一段时间过后此类错误将导致书写内容移位。</P>
<P>我将所有绑定信息存储在 <B>Stroke </B>对象的 ExtendedProperties 中。应用程序可以使用 
<B>Stroke.ExtendedProperties</B> 存取存储在 <B>Stroke</B> 
对象中的自定义数据。这些自定义数据将自动与对象序列化。因此，我在 <B>ExtendedProperties</B> 
中存储了以下六条信息：<B>Stroke</B> 边框的位置 (x,y) 和大小（长，宽），以及 <B>PuzzleGrid</B> 
控件的当前大小（长，宽）。<B>ExtendedProperties</B> 中 Add 方法的第一个参数是 
<B>Guid</B>，可用于识别所创建的特定扩展属性。它可以是您需要的任何 <B>Guid</B>，但如果您将 <B>Strokes</B> 
序列化以永久存储，供以后运行应用程序时检索 <B>Strokes</B>，则请确保用于扩展属性的 <B>Guids</B> 
在每次运行应用程序时保持一致。</P><PRE>Rectangle boundingBox = s.GetBoundingBox();
using(Graphics graphics = CreateGraphics())
{
InkSpaceToPixelSpace(graphics, ref boundingBox);
s.ExtendedProperties.Add(OriginalStrokeBoundRectXGuid,  
boundingBox.X);
s.ExtendedProperties.Add(OriginalStrokeBoundRectYGuid,  
boundingBox.Y);
s.ExtendedProperties.Add(OriginalStrokeBoundRectWidthGuid,  
boundingBox.Width);
s.ExtendedProperties.Add(OriginalStrokeBoundRectHeightGuid,  
boundingBox.Height);
s.ExtendedProperties.Add(OriginalClientRectWidthGuid,  
ClientRectangle.Width);
s.ExtendedProperties.Add(OriginalClientRectHeightGuid,  
ClientRectangle.Height);
}
</PRE>
<P>接下来，当调整 <B>PuzzleGrid</B> 的大小时，我使用此信息修改各个 <B>Stroke</B>。</P><PRE>protected override void OnResize(EventArgs e)
{
_cachedEmSize = -1;
ResizeScratchpadInk();
base.OnResize(e);
}
internal void ResizeScratchpadInk()
{
if (_inkOverlay != null)
  {
Rectangle currentClientRect = ClientRectangle;
using(Strokes scratchpadStrokes = ScratchpadStrokes)  
    {
foreach(Stroke s in scratchpadStrokes)
      {
int originalBoundsX = (int)s.ExtendedProperties[
OriginalStrokeBoundRectXGuid].Data;
int originalBoundsY = (int)s.ExtendedProperties[
OriginalStrokeBoundRectYGuid].Data;
int originalBoundsWidth = (int)s.ExtendedProperties[
OriginalStrokeBoundRectWidthGuid].Data;
int originalBoundsHeight = (int)s.ExtendedProperties[
OriginalStrokeBoundRectHeightGuid].Data;
int originalClientRectWidth =  
(int)s.ExtendedProperties[
OriginalClientRectWidthGuid].Data;
int originalClientRectHeight =  
(int)s.ExtendedProperties[
OriginalClientRectHeightGuid].Data;
double scaleX = currentClientRect.Width /  
(double)originalClientRectWidth;
double scaleY = currentClientRect.Height /  
(double)originalClientRectHeight;

Rectangle newBounds = new Rectangle(
(int)(originalBoundsX*scaleX),  
(int)(originalBoundsY*scaleY),  
(int)(originalBoundsWidth*scaleX),  
(int)(originalBoundsHeight*scaleY));

using(Graphics graphics = CreateGraphics())
        {                
PixelSpaceToInkSpace(graphics, ref newBounds);
s.ScaleToRectangle(newBounds);
        }
      }
    }
  }
}
</PRE>
<P>对于每个笔画，将计算控件当前大小与笔画创建时控件大小的比例（请注意必须对每个笔画进行计算，因为创建不同笔画时的“初始”控件大小可能不同，即 
<B>PuzzleGrid</B> 可能在不同笔画创建过程中调整了大小）。然后通过此比例和笔画的初始边界计算笔画的新边界。再将此信息提供给<B> 
Stroke</B> 中的 ScaleToRectangle 方法，该方法在 <B>Stroke</B> 中执行实际的调整操作。</P>
<P>当在普通模式和便签簿模式间切换时，在自己的集合中拥有便签簿笔画将非常方便。应用程序在这两种模式下运行产生的视觉区别之一是，在普通模式下时，便签簿手写内容灰显，而在便签簿模式下时，手写内容为纯黑色。通过更改每个便签簿 
<B>Stroke</B> 实例中的 DrawingAttributes 可以实现。例如，要将各个笔画的颜色更改为灰色，可使用以下代码。</P><PRE>using(Strokes strokes = ScratchpadStrokes)
{
foreach(Stroke s in strokes)
  {
s.DrawingAttributes.Color = Color.Gray;
  }
}
</PRE>
<P>当用户将数字输入单元格时，在自己的集合中拥有便签簿笔画也会非常方便。当用户将数字输入单元格时，我会调用 <B>SetStateCell</B> 
方法（如前所示）。该方法接着会调用 <B>ClearTabletStateCell</B> 方法。<B>ClearTabletStateCell</B> 
将删除单元格中的所有便签簿笔画，因为用户刚刚向目标单元格输入了一个逻辑演绎数字，因此可以删除与该单元格相关的记录，以避免不必要的干扰。<B>ClearTabletStateCell</B> 
的工作方式是，遍历便签簿笔画集合中的所有笔画，删除那些位于单元格中的笔画。</P><PRE>private void ClearTabletStateCell(Point cell)
{
if (_inkOverlay != null)
  {
//删除该单元格中所有的便签簿笔画
using(Strokes strokes = ScratchpadStrokes)
    {
for(int i=strokes.Count-1; i&gt;=0; --i)
      {
Stroke s = strokes[i];
if (GetCellFromStroke(s) == cell)
        {
strokes.RemoveAt(i);
_inkOverlay.Ink.DeleteStroke(s);
        }
      }
    }
  }
}
</PRE>
<DIV><A 
href="http://msdn.microsoft.com/zh-cn/library/aa480686.aspx#mainSection"><IMG 
alt="" 
src="Microsoft Sudoku：优化 UMPC 应用程序的点触和手写操作.files/Aa480686.arrow_px_up(zh-cn,MSDN.10).gif" 
border=0> </A><A 
href="http://msdn.microsoft.com/zh-cn/library/aa480686.aspx#mainSection">返回页首</A> 
<BR></DIV>
<H3 id=E5AA>橡皮擦支持</H3>
<P>笔式橡皮擦支持是受到许多 Tablet PC 
支持的一个便利功能。将您的触控笔倒转过来，您可以使用另一端作为一个虚拟的橡皮擦，前提是该应用程序支持此概念。Sudoku 可以实现这一点。</P>
<P><B>InkOverlay</B> 类提供了 InkOverlayEditingMode 类型的 EditingMode 属性。该属性的默认值是 
<B>InkOverlayEditingMode.Ink</B>。但是，当设置为 <B>InkOverlayEditingMode.Delete</B> 
时，光标会变成一个橡皮擦，在现有的笔画上拖拽可以将其删除，而不是创建和保存新的手写笔画。通常，应用程序会在事件处理器中为 <B>InkOverlay</B> 
上的 CursorInRange 事件检查和设置该属性，而 Sudoku 正是这样做的。InkCollectorCursorInRangeEventArgs 
实例作为 <B>CursorInRange</B> 事件的事件处理器一个参数而提供，该实例拥有一个 <B>Cursor</B> 属性，会返回一个 
Microsoft.Ink.Cursor 实例，而该对象具有布尔类型的 Inverted 
属性，会返回触控笔是否上面向下，从而确定用户是否尝试使用橡皮擦。我的事件处理器通过适当的设置 <B>InkOverlay</B> 上的 
<B>EditingMode</B> 来对此做出响应。</P>
<P><B>请注意</B> 空中数据包检测用于电磁数字转换器和鼠标，但不用于触摸式数字转换器。 </P>
<P>收到删除笔画后，将触发 <B>InkOverlay</B> 上的 <B>Stroke</B> 
事件。但是，此刻会执行清除橡皮擦下单元格的代码，而不是执行多笔画识别逻辑，在 <B>EditingMode</B> 设置为 
<B>InkOverlayEditingMode.Ink</B> 时会执行该逻辑。</P><PRE>Point currentStrokeCell = GetCellFromStroke(e.Stroke);
if (CanClearCell(currentStrokeCell))  
{
ClearStateCell(currentStrokeCell);
}
</PRE>
<P>此代码仅清除一个单元格，因为 <B>GetCellFromStroke</B> 将基于 <B>Stroke</B> 
上的边框挑选最有可能的目标单元格。但是，如果玩家拖动橡皮擦跨越多个单元格，则他们很可能想要清除所有接触到的单元格。要在用户拖动橡皮擦跨越多个单元格时清除所有接触的单元格，可将类似的逻辑添加到 
<B>InkOverlay</B> 上 <B>NewPackets</B> 事件的事件处理器中。</P><PRE>if (_mode == PuzzleGridMode.Eraser || e.Cursor.Inverted)
{
if (e.PacketData.Length &gt;= 2)
  {
Point cell = TabletToCell(
new Point(e.PacketData[0], e.PacketData[1]));
if (CanClearCell(cell) &amp;&amp; State[cell].HasValue)  
    {
ClearStateCellWithInvalidation(cell);
    }
if (_selectedCell.HasValue)  
    {
InvalidateCell(_selectedCell.Value);
    }
SetSelectedCell(cell);
InvalidateCell(_selectedCell.Value);
  }
}
</PRE>
<P>当处于便签簿模式下，我将应用一个不同的逻辑。<B>InkOverlay</B> 上 <B>InkOverlayEditingMode.Delete</B> 
的默认处理是清除光标下的任何笔画。这正是在便签簿模式下我想要的行为，因此我让 <B>InkOverlay</B> 
来完成这项作业。只需两步。第一，我需要在删除笔画前设置一个撤消点，这样就可以使用撤消来恢复删除的笔画。第二，我需要从自定义收集中手动清除已删除的笔画。我可以在 
<B>InkOverlay</B> 上 StrokesDeleting 事件的事件处理器中实现这两步。</P><PRE>private void HandleStrokesDeleting(
object sender, InkOverlayStrokesDeletingEventArgs e)
{
SetUndoCheckpoint();
using(Strokes normalStrokes = NormalStrokes)  
  {
normalStrokes.Remove(e.StrokesToDelete);
  }
using(Strokes scratchpadStrokes = ScratchpadStrokes)
  {
scratchpadStrokes.Remove(e.StrokesToDelete);
  }
}
</PRE>
<P>如前所述，玩家还可以使用擦除手势清除以前在单元格中输入的数字（玩家还可以使用擦除手势清除便签簿模式下创建的注释），如图 15 所示。</P>
<P><IMG alt="" 
src="Microsoft Sudoku：优化 UMPC 应用程序的点触和手写操作.files/Aa480686.tbconsudokusamplefigure15(zh-cn,MSDN.10).gif"> 
</P>
<DIV><B>图 15. 擦除数字将其清除</B> </DIV>
<P>在正常模式下，这表示通过确定擦除手势所擦除的单元格，然后清除该单元格来对 <B>Gesture</B> 
事件做出响应。但是，需要注意的一个重要事情是，清除单元格的操作需要设置撤消检查点。该撤消检查点包括当前存储在 <B>InkOverlay</B> 
中的所有手写内容。此时，手势的笔画仍在覆盖图中，因此，在清除单元格前我将明确删除手势的笔画。</P><PRE>void HandleGestureInNormalMode(
object sender, InkCollectorGestureEventArgs e)
{
switch (e.Gestures[0].Id)
  {
case ApplicationGesture.Scratchout:
Point cell = GetCellFromStroke(e.Strokes[0]);
_inkOverlay.Ink.DeleteStrokes(e.Strokes);
RecognizePreviousNormalStrokes();
if (CanClearCell(cell)) ClearStateCell(cell);
break;
default:
e.Cancel = true;
break;
  }
Invalidate();
}
</PRE>
<P>便签簿模式下的擦除手势处理更加容易。应用程序将检索任何与擦除手势笔画相交叉的便签簿笔画，并将其删除。</P>
<DIV><A 
href="http://msdn.microsoft.com/zh-cn/library/aa480686.aspx#mainSection"><IMG 
alt="" 
src="Microsoft Sudoku：优化 UMPC 应用程序的点触和手写操作.files/Aa480686.arrow_px_up(zh-cn,MSDN.10).gif" 
border=0> </A><A 
href="http://msdn.microsoft.com/zh-cn/library/aa480686.aspx#mainSection">返回页首</A> 
<BR></DIV>
<H3 id=E2AA>胜利动画</H3>
<P>所有游戏都会在玩家获胜之后，对其所取得成绩给予鼓励。当然，在您填完最后一个单元格并得意于您的逻辑推理能力时，已经有了极大的自我成就感，但如果此刻游戏能进一步加深这种美好感觉，给您一个恰到好处的赞赏，那么您的感觉肯定会更好，比如 
Microsoft 的纸牌游戏 Solitaire 就是这么做的（参见图 16）。</P>
<P><IMG alt="" 
src="Microsoft Sudoku：优化 UMPC 应用程序的点触和手写操作.files/Aa480686.tbconsudokusamplefigure16(zh-cn,MSDN.10).gif"> 
</P>
<DIV><B>图 16. 游戏获胜后的 Solitaire 窗口</B> </DIV>
<P>我实现了几个待选 Sudoku 获胜动画，而且我的 fianc??e Tamara（Sudoku 迷们，我最好的 beta 
版测试者）也为我提供了几个不错的建议，最后我决定使用这样一个动画：许多单元格在游戏面板上飞舞，翻转、旋转、飘动，同时用金黄色的文字显示“祝贺您！”。另外，除了旋转的飞片外，玩家还想要看到刚刚完成的游戏，获得这一反馈信息后，我将完成游戏面板的淡化图像保留在旋转数字之后。您可以在图 
17 中看到此示例。</P>
<P><IMG alt="" 
src="Microsoft Sudoku：优化 UMPC 应用程序的点触和手写操作.files/Aa480686.tbconsudokusamplefigure17(zh-cn,MSDN.10).gif"> 
</P>
<DIV><B>图 17. 完成填数游戏后的 Sudoku 窗口</B> </DIV>
<P>我将动画内置于名为 <B>WinningAnimation</B> 的一个控件中，该控件位于 Controls 文件夹的 
WinningAnimation.cs 文件中。初始化主窗体后，<B>WinningAnimation</B> 的一个实例会被添加到 
<B>PuzzleGrid</B> 上的 <B>Controls</B> 集合中，然后停靠并填充父网格控件，并转入隐藏状态。</P><PRE>_winningAnimation = new WinningAnimation(thePuzzleGrid);
thePuzzleGrid.Controls.Add(_winningAnimation);
_winningAnimation.Dock = DockStyle.Fill;
_winningAnimation.Visible = false;
_winningAnimation.BringToFront();
</PRE>
<P>当玩家完成填数游戏后，会显示动画控件，从而启动动画。</P>
<P>动画本身是以 sprite 系统为基础的。Sprite 
是对那些集成到较大场景图中的图像或动画的一个有趣称呼。通常，它们可以保持自己的状态，可以来回移动并与其它 sprite 
进行交互，还可以进行如自我呈现之类的操作。对于此动画来说，每个飞舞的单元格就是其自身的 sprite。</P><PRE>internal class ImageSprite :IDisposable
{
Point _location;
float _angle;
float _flipPosition;

Size _velocity;
float _angularVelocity;
float _flipSpeed;
bool _rotateAroundCenter;

private Bitmap _bmp;
  ...
}
</PRE>
<P>我在控件内的起始位置创建了各个 sprite，还创建了一个 <B>Bitmap</B>，其中包含将由 sprite 
呈现的主图像。动画演示过程中，sprite 
将保持其当前的位置、线速度、角速度以及<I>翻转速度</I>（这是我起的名称，即围绕另一个旋转轴的角速度）。我还为每个 sprite 配置了布尔值，该值用于指定 
sprite 是围绕中心旋转还是围绕某个角度旋转。</P>
<P>每个 sprite 都可自我呈现。呈现父控件时，它会为每个 sprite 提供目标 <B>Graphics</B> 对象，并且由 sprite 处理对该 
<B>Graphics</B> 对象的绘制。</P><PRE>public void Paint(Graphics graphics)
{
using(Matrix mx = new Matrix())
  {
GraphicsState gs = graphics.Save();
if (_rotateAroundCenter) mx.Translate(
-_bmp.Width/2, -_bmp.Height/2, MatrixOrder.Append);  
mx.Rotate(_angle, MatrixOrder.Append);
if (_rotateAroundCenter) mx.Translate(
_bmp.Width/2, _bmp.Height/2, MatrixOrder.Append);  
mx.Translate(_location.X, _location.Y, MatrixOrder.Append);
graphics.Transform = mx;

//绘制图像
float flipMult = ((float)
Math.Cos(_flipPosition*Math.PI/180.0));
if (flipMult &gt; 0.001 || flipMult &lt; -0.001)
    {
graphics.DrawImage(_bmp,  
new RectangleF(0, 1-Math.Abs(flipMult),  
_bmp.Width, _bmp.Height*flipMult),  
new RectangleF(0, 0, _bmp.Width, _bmp.Height),  
GraphicsUnit.Pixel);
    }

graphics.Restore(gs);
  }
}
</PRE>
<P>下面将进行多个操作。首先，我创建了一个 System.Drawing.Drawing2D.Matrix 
对象。如果您不熟悉线性代数及其对图形的影响，可使用常用的矩阵来从一个坐标空间向另一个转换。通过 <B>Matrix</B> 
类可以方便的应用转换（如旋转和转换），并可以为您处理所有底层的数学运算。而通过 Graphics 类可以方便的使用应用的转换进行绘制。</P>
<P>开始前，我保存了 <B>Graphics</B> 对象的当前状态，以便我在当前转换下完成必要的绘制后可以将其恢复。然后，旋转根据 sprite 
的当前角度创建的矩阵。注意，sprite 围绕它的角进行旋转，角的坐标为 [0,0]。如果某个特定 sprite 
必须围绕它的中心旋转，那么我可以在旋转前应用一个转换，将 sprite 的中心移到 [0,0]。然后，在应用旋转后，撤消转换。</P>
<P>在对 sprite 进行适当的旋转后，我将其移动到正确的位置（转换即基于 sprite 当前位置的转换）。最后，通过使用 
<B>Graphics.DrawImage</B> 来绘制 sprite。为了实现 sprite 
的翻转效果，我更改了目标矩形的垂直尺寸（想想手风琴在演奏时的拉开与收缩）。当垂直尺寸为负数时，<B>DrawImage</B> 
会将图像呈现为正面向下，这将会创造出一种使玩家看穿飞片背面的效果。</P>
<P>借助于 sprite 的自我呈现能力，整个动画都是自行控制的。</P><PRE>protected override void OnPaint(PaintEventArgs pe)
{
//进行基本绘画
base.OnPaint(pe);

//绘制基本底层图像
if (_underImage != null)  
  {
pe.Graphics.DrawImage(_underImage, 0, 0);
  }

//呈现所有 sprite
if (_sprites != null &amp;&amp; _sprites.Count &gt; 0)
  {
for(int i=_sprites.Count-1; i&gt;=0; --i)
    {
ImageSprite s = (ImageSprite)_sprites[i];
s.Paint(pe.Graphics);
    }
  }

//显示祝贺文本
string text = ResourceHelper.PuzzleSolvedCongratulations;
if (_sf != null &amp;&amp; text != null &amp;&amp; text.Length &gt; 0)
  {
float emSize = GraphicsHelpers.GetMaximumEMSize(text,
pe.Graphics, Font.FontFamily, Font.Style,  
ClientRectangle.Width, ClientRectangle.Height);
using(Font f = new Font(Font.FontFamily, emSize))
    {
pe.Graphics.DrawString(text, f, Brushes.Black,  
new RectangleF(2, 2, ClientRectangle.Width,  
ClientRectangle.Height), _sf);
pe.Graphics.DrawString(text, f, Brushes.Gray,  
new RectangleF(-1, -1, ClientRectangle.Width,  
ClientRectangle.Height), _sf);
pe.Graphics.DrawString(text, f, Brushes.Yellow,  
new RectangleF(0, 0, ClientRectangle.Width,  
ClientRectangle.Height), _sf);
    }
  }
}
</PRE>
<P>该方法从呈现底层图像开始（我将立即对其进行讨论）；然后继续遍历所有 sprite，并利用 sprite 的 <B>Paint</B> 
方法（如上所述）来呈现每个 sprite。最后呈现祝贺文本。该文本通过三次调用 
Graphics.DrawString、两次绘制文本的背景或阴影以及一次绘制黄色覆盖图来呈现。由于控件大小可能有所不同，因此必须使用字体的大小来呈现文本。指定的字体大小可通过使用 
<B>GraphicsHelpers</B> 类上的 <B>GetMaximumEMSize</B> 
静态方法来检索。此方法接受呈现的文本、目标图形对象、指定的字体系列和类型以及正好适合文本大小的矩形宽度和高度。然后，它会返回最大的字体大小，使指定文本可以指定字体系列和类型，正好适合该边框。</P><PRE>public static float GetMaximumEMSize(string text,
Graphics graphics, FontFamily fontFamily, FontStyle fontStyle,  
float width, float height)
{
const float MAX_ERROR = .25f;
float curMin = 1.0f, curMax = width;
float emSize = ((curMax - curMin) / 2f) + curMin;
while(curMax - curMin &gt; MAX_ERROR &amp;&amp; curMin &gt;= 1)
  {
using (Font f = new Font(fontFamily, emSize, fontStyle))
    {
SizeF size = graphics.MeasureString(text, f);
bool textFits = size.Width &lt; width &amp;&amp; size.Height &lt; height;
if (textFits &amp;&amp; emSize &gt; curMin) curMin = emSize;
else if (!textFits &amp;&amp; emSize &lt; curMax) curMax = emSize;
    }
emSize = ((curMax - curMin) / 2f) + curMin;
  }
return curMin;
}
</PRE>
<P>我将使用二进制搜索来确定最大字体大小。假设最小字体大小为一点，同时假设最大字体大小为边框的宽度。该方法首先测试如果以最大与最小字体中间的一个点大小来呈现，则指定字体的指定文本是否适合边界矩形。如果呈现的文本适合，那么 
<B>GetMaximumEMSize</B> 
将尝试介于中间点与最大尺寸中间的一个更大的值。如果不适合，那么该方法将尝试介于中间点与最小尺寸中间的一个值。此等分方法将继续测试，直至当前最小和最大尺寸小于彼此间很小的距离（每点 
.25）为止，此时该方法将返回最小值作为选定字体大小。</P>
<P>此方法非常方便，不仅可用于呈现祝贺文本，还可用于在应用程序中呈现所有文本。改变窗体大小时，呈现文本（例如标签和按钮）的应用程序中所有控件都将根据 
<B>GetMaximumEMSize</B> 的结果来重新计算使用的字体大小。这就是在改变窗体大小时能够自如缩放字体（例如，数字按钮上的数字）的原因。</P>
<P>显示 <B>WinningAnimation</B> 控件时，应用程序将创建 sprite。控件及时给 <B>PuzzleGrid</B> 
控件的当前外观拍下快照（这就是当它们旋转和来回移动时在 sprite 下所呈现的图像）。然后，它从那个较大的位图中创建了 81 
个单独的位图，并将其中的每个位图指定给一个新的 sprite。</P><PRE>using(Bitmap bmp = new Bitmap(
_grid.ClientRectangle.Width, _grid.ClientRectangle.Height))
{
//给底层的填数游戏网格拍快照
using(Graphics g = Graphics.FromImage(bmp))
  {
_grid.DrawToGraphics(g, _grid.ClientRectangle);
  }

//以拉出底层网格快照的一部分为基础，
//分别设置各个 sprite
for(int i=0; i&lt;_grid.State.GridSize; i++)
  {
for(int j=0; j&lt;_grid.State.GridSize; j++)
    {
RectangleF cellRect = PuzzleGrid.GetCellRectangle(
_grid.BoardRectangle, new Point(i,j));
Bitmap smallImage = new Bitmap( (int)cellRect.Width,  
(int)cellRect.Height, PixelFormat.Format32bppPArgb);
using(Graphics g = Graphics.FromImage(smallImage))
      {
g.DrawImage(bmp, 0, 0, cellRect, GraphicsUnit.Pixel);
      }
ImageSprite s = new ImageSprite(smallImage);
      ...
    }
  }
}
</PRE>
<P>显示控件时，我还创建了一个计时器。在我创建 sprite 时，每个 sprite 
都被初始化至控件中的适当位置，这样，它们正好覆盖到其在底层网格中的相应位置。然后，每个 sprite 会被指定随机的线速度、角速度和翻转速度。每个 sprite 
还会被随机指定围绕中心旋转还是围绕角旋转。每次定时器开始计时时（每秒 24 次，假设 CPU 持续运转），我会使用各个 sprite 的 
<B>Update</B> 方法，相应的更改 sprite 的状态（<B>Update</B> 方法对 sprite 
使用不同速度来更新其位置信息），然后该控件使其自身无效，开始重新绘制。</P><PRE>for(int i=_sprites.Count-1; i&gt;=0; --i)
{
ImageSprite s = (ImageSprite)_sprites[i];
s.Update();

Rectangle bounds = ClientRectangle;
if (s.Location.X &gt; bounds.Right + s.Image.Width ||  
s.Location.X &lt; -s.Image.Width ||
s.Location.Y &gt; bounds.Bottom + s.Image.Width ||  
s.Location.Y &lt; -s.Image.Width)  
  {
_sprites.RemoveAt(i);
s.Dispose();
  }
}
if (_sprites.Count == 0) _timer.Stop();
Invalidate();
</PRE>
<P>当 sprite 离开控件的可视区域后，我将对其进行处理并从 sprite 集合中将其删除。当不再显示 sprite 
时，我将停止计时器。出于对性能因素的考虑，停止计时器是很重要的，而对于需要考虑功率损耗的移动设备来说，停止计时器更是尤为重要。频繁地触发计时器（如一秒钟多次）会妨碍系统进入节能模式。因此，如果您必须使用频繁触发的计时器（在游戏和动画中并不常见），请确保在您完全不需要运行计时器时将其禁用。</P>
<DIV><A 
href="http://msdn.microsoft.com/zh-cn/library/aa480686.aspx#mainSection"><IMG 
alt="" 
src="Microsoft Sudoku：优化 UMPC 应用程序的点触和手写操作.files/Aa480686.arrow_px_up(zh-cn,MSDN.10).gif" 
border=0> </A><A 
href="http://msdn.microsoft.com/zh-cn/library/aa480686.aspx#mainSection">返回页首</A> 
<BR></DIV>
<H3 id=EYAA>保存状态和设置</H3>
<P>Sudoku 
在用户本地应用程序数据存储中使用两个文件来保存应用程序的状态。游戏使用一个文件来存储应用程序设置，使用另一个文件来存储未完成的填数游戏的状态，这样用户就可以在以后运行程序时继续完成此填数游戏。所有的设置和状态都被序列化到使用 
BinaryFormatter<B> </B>的文件中，并且又从这些文件中被反序列化，BinaryFormatter 位于 
<B>System.Runtime.Serialization.Formatters.Binary</B> 命名空间中。</P>
<P>设置文件包含有关当前已配置选项（是否显示错误的值、建议下一步尝试完成哪个单元格、删除正在进行的填数游戏之前给出提示以及创建新的对称填数游戏）、主窗体的当前大小和位置以及当前填数游戏难度级别的信息。为简化表示，我省略了异常处理，我的 
<B>SaveSettings</B> 实现代码如下：</P><PRE>private void SaveSettings()
{
BinaryFormatter formatter = new BinaryFormatter();
string path = Environment.GetFolderPath(
Environment.SpecialFolder.LocalApplicationData) +  
SAVED_SETTINGS_USER_PATH;

DirectoryInfo parentDir = Directory.GetParent(path);
if (!parentDir.Exists) parentDir.Create();
  
ConfigurationOptions options =  
_optionsDialog.ConfiguredOptions;
PuzzleDifficulty difficulty = _puzzleDifficulty;
Rectangle windowBounds = GetRestoreBounds(this);
bool maximized = (WindowState == FormWindowState.Maximized);
using(Stream s = File.OpenWrite(path))
  {
formatter.Serialize(s, options);
formatter.Serialize(s, difficulty);
formatter.Serialize(s, windowBounds);
formatter.Serialize(s, maximized);
  }
}
</PRE>
<P><B>LoadSettings</B> 方法将执行正好相反的工作，即从设置文件将此信息反序列化到对象中，并将这些信息恢复回窗体和配置的选项中。</P>
<P>我的 <B>SaveStateFile</B> 方法是用来保存当前正在进行的填数游戏的状态，其代码如下（我还是删除了异常处理）。</P><PRE>internal void SaveStateFile()
{
if (thePuzzleGrid.PuzzleHasBeenModified)
  {
string path = Environment.GetFolderPath(
Environment.SpecialFolder.LocalApplicationData) +  
SAVED_STATE_USER_PATH;
DirectoryInfo parentDir = Directory.GetParent(path);
if (!parentDir.Exists) parentDir.Create();
using(Stream s = File.OpenWrite(path))
    {
BinaryFormatter formatter = new BinaryFormatter();
formatter.Serialize(s,
Assembly.GetEntryAssembly().GetName().Version);
formatter.Serialize(s, thePuzzleGrid.State);
formatter.Serialize(s, thePuzzleGrid.OriginalState);
formatter.Serialize(s, thePuzzleGrid.UndoStates);
formatter.Serialize(s, thePuzzleGrid.InkData);
    }
  }
}
</PRE>
<P>执行序列化前，<B>SaveStateFile</B> 方法先检查 
<B>PuzzleGrid</B>，确定是否正在进行填数游戏，也就是检查游戏是否已在某些方面相对于原始状态有所修改，但还没有完成游戏。当 
<B>SaveStateFile</B> 创建状态文件时，它会在其中存储 5 条信息。首先，它将存储当前的 Sudoku 
版本，这始终是一个很好的做法，这样在将来的版本中，如果您想要更改存储在文件上的信息，可与先前版本的状态文件后向兼容。然后，它将存储当前的 
<B>PuzzleState</B>，接着存储原始 <B>PuzzleState</B>（提供给 
<B>PuzzleGrid</B>）；后者是必须存储的，这样就可以知道当前填数游戏中的哪些数字是最初存在的。接下来，将撤消堆栈存储到文件中。最后，将 
<B>PuzzleGrid</B> 覆盖上的所有 <B>Ink</B> 数据序列导出。<B>PuzzleGrid</B> 上的 <B>InkData</B> 
属性会返回一个包含序列化手写内容的字节数组。</P><PRE>_inkOverlay.Ink.Save(PersistenceFormat.InkSerializedFormat)
</PRE>
<P>就这些设置而言，加载过程（其所有目的和用途）是与保存方法正好相反的过程。加载过程就是从文件中加载每条信息并将信息恢复到 <B>PuzzleGrid</B> 
中。</P>
<DIV><A 
href="http://msdn.microsoft.com/zh-cn/library/aa480686.aspx#mainSection"><IMG 
alt="" 
src="Microsoft Sudoku：优化 UMPC 应用程序的点触和手写操作.files/Aa480686.arrow_px_up(zh-cn,MSDN.10).gif" 
border=0> </A><A 
href="http://msdn.microsoft.com/zh-cn/library/aa480686.aspx#mainSection">返回页首</A> 
<BR></DIV>
<H3 id=EVAA>电源问题</H3>
<P>在实现移动应用程序时，电源是必须考虑的问题。用户应该知道当设备电量将要用尽时，应想尽一切办法来防止用户丢失数据。当 Sudoku 
检测到低电量或系统将要休眠或进入待机状态时，它会保存当前的填数游戏。此功能通过覆盖 WndProc 方法（Control 中的主方法，用于处理 Windows 
消息）在主窗体中实现，这样便可在应用程序收到 WM_POWERBROADCAST 消息时保存当前游戏（使用 
Microsoft.Win32.SystemEvents 类可实现类似功能）。</P><PRE>protected override void WndProc(ref Message m)
{
base.WndProc (ref m);

const int WM_POWERBROADCAST = 0x218;
const int PBT_APMSUSPEND = 0x0004;
const int PBT_APMSTANDBY = 0x0005;
const int PBT_APMBATTERYLOW = 0x0009;

switch(m.Msg)
  {
case WM_POWERBROADCAST:
switch((int)m.WParam)
      {
case PBT_APMBATTERYLOW:
case PBT_APMSTANDBY:
case PBT_APMSUSPEND:
SaveStateFile();
break;
      }
break;
    ...
}
</PRE>
<P>如果当前会话丢失，那么通过这种方法玩家将可以找回中断的填数游戏。</P>
<P>另外，几个其他的非电源相关消息也在 <B>WndProc</B> 覆盖中处理。例如，我在前文中提到的，在设置更改时，我会调用 
<B>SetHandedness</B> 来更新 UI，以体现用户的当前左右手使用习惯设置。当 Windows 向应用程序发送 
WM_SETTINGSCHANGE 消息时，Windows 会通知其设置更改。我的 <B>WndProc</B> 实现会检查是否对 
SPI_SETMENUDROPALIGNMENT 设置做了更改。如果对该设置做了更改，则它将调用 <B>SetHandedness.</B>。 </P><PRE>case WM_SETTINGSCHANGE:
if (SPI_SETMENUDROPALIGNMENT == (int)m.WParam) SetHandedness();
break;
</PRE>
<DIV><A 
href="http://msdn.microsoft.com/zh-cn/library/aa480686.aspx#mainSection"><IMG 
alt="" 
src="Microsoft Sudoku：优化 UMPC 应用程序的点触和手写操作.files/Aa480686.arrow_px_up(zh-cn,MSDN.10).gif" 
border=0> </A><A 
href="http://msdn.microsoft.com/zh-cn/library/aa480686.aspx#mainSection">返回页首</A> 
<BR></DIV>
<H3 id=ESAA>扩展 Sudoku </H3>
<P>有多种扩展和增强随附 Sudoku 代码的方法。下面给出几种方法连同一些代码供您参考。</P>
<P>扩展撤消支持</P>
<P>实现代码时，撤消支持是非常有用的，但是您可以更进一步发挥其用途。在玩 Sudoku 
时，我偶尔会犯错误（很吃惊，是么？），我做了错误的推断，没有看到我所填写的数字在相关的行、列或方框中已经存在，或者犯一些其他类似的错误。一旦犯了错误，游戏就会失败，但通常会在前进许多步之后才意识到错误，因此，我会懒于处理所面临的困境。我真正需要的不只是一个撤消功能，而是需要一种方法，可以返回到我最初犯错误的地方。对于已经实现的撤消功能和我前面讨论的 
<B>Solver</B> 功能，这两种功能都是很容易实现的。</P><PRE>public void UndoUntilSolvableState()
{
SolverOptions options = new SolverOptions();
while(Solver.Solve(State, options).Status !=  
PuzzleStatus.Solved &amp;&amp; Undo());
}
</PRE>
<P>另一种有用的功能是重做支持。您可以为重做添加另一个 <B>PuzzleStateStack</B> 
支持，该支持使您可以在撤消时将状态推入堆栈中。试一试吧。只要记住，在您进行撤消操作时，需要将状态推回撤消堆栈中。</P>
<DIV><A 
href="http://msdn.microsoft.com/zh-cn/library/aa480686.aspx#mainSection"><IMG 
alt="" 
src="Microsoft Sudoku：优化 UMPC 应用程序的点触和手写操作.files/Aa480686.arrow_px_up(zh-cn,MSDN.10).gif" 
border=0> </A><A 
href="http://msdn.microsoft.com/zh-cn/library/aa480686.aspx#mainSection">返回页首</A> 
<BR></DIV>
<H3 id=EPAA>打印游戏</H3>
<P>扩展 Sudoku 对打印当前填数游戏的支持几乎不需要任何额外的工作。例如，您可以选择一个特定键，扩展 <B>PuzzleGrid</B> 上的 
<B>OnKeyDown</B> 方法，以在按下此键时执行代码。</P><PRE>using(PrintDialog pd = new PrintDialog())
using(PrintDocument doc = new PrintDocument())
{
doc.PrintPage += new PrintPageEventHandler(HandlePrintPage);
pd.Document = doc;
if (pd.ShowDialog() == DialogResult.OK) doc.Print();
}
</PRE>
<P>然后，您可以使用下列代码处理 <B>PrintPage</B> 事件。</P><PRE>private void Document_PrintPage(object sender, PrintPageEventArgs e)
{
DrawToGraphics(e.Graphics, Bounds);
e.HasMorePages = false;
}
</PRE>
<P>这就是对扩展打印支持所做的全部工作。当然，这不会产生最佳的输出效果。填数游戏的大小将基于 <B>PuzzleGrid</B> 
的当前大小，它不会位于页面中央，并且打印的输出将包括像当前建议的单元格之类的内容。此问题可通过创建一个新的完全用于打印支持的 <B>PuzzleGrid</B> 
来解决。</P><PRE>private void Document_PrintPage(object sender, PrintPageEventArgs e)
{
using(PuzzleGrid pg = new PuzzleGrid())
  {
pg.State = State;
pg.ShowSuggestedCells = false;
pg.ShowIncorrectNumbers = false;
if (e.MarginBounds.Width &gt; e.MarginBounds.Height)
    {
pg.Height = e.MarginBounds.Height;
pg.Width = pg.Height;
    } 
else
    {
pg.Width = e.MarginBounds.Width;
pg.Height = pg.Width;
    }
using(Matrix m = new Matrix())
    {
GraphicsState state = e.Graphics.Save();
m.Translate((e.PageBounds.Width - pg.Width) / 2,  
(e.PageBounds.Height - pg.Height) / 2);
e.Graphics.Transform = m;
pg.DrawToGraphics(e.Graphics,  
new Rectangle(0, 0, pg.Width, pg.Height));
e.Graphics.Restore(state);
    }
  }
e.HasMorePages = false;
}
</PRE>
<P>您可以进一步打印出填数游戏的整个集合。每次调用 <B>PrintPage</B> 处理器时，会使用<B> Generator</B> 类创建一个全新的 
<B>PuzzleState</B>（而不是使用当前网格的 
<B>PuzzleState</B>，如上例所示），然后使用此代码来打印输出。这样，您就可以打印出您想要的任何数量的填数游戏，将 
<B>HasMorePages</B> 设置为 <B>true</B> 直至您打印出所需的数量为止。 </P>
<DIV><A 
href="http://msdn.microsoft.com/zh-cn/library/aa480686.aspx#mainSection"><IMG 
alt="" 
src="Microsoft Sudoku：优化 UMPC 应用程序的点触和手写操作.files/Aa480686.arrow_px_up(zh-cn,MSDN.10).gif" 
border=0> </A><A 
href="http://msdn.microsoft.com/zh-cn/library/aa480686.aspx#mainSection">返回页首</A> 
<BR></DIV>
<H3 id=EMAA>支持多处理器</H3>
<P>生成游戏非常耗用 CPU 资源。对于拥有多个处理器的系统来说，有必要对 Sudoku 
填数游戏生成的基础架构进行扩展以便充分利用这些多处理器（当前的填数游戏生成逻辑是单线程的）。为此多处理器添加支持实际上比听起来更容易些，因为提供给用户的每个填数游戏实际上是从生成的多个填数游戏结果中所选择的最难的一个。目前，该操作是通过连续循环完成的。</P><PRE>private PuzzleState GenerateInternal()
{
ArrayList puzzles = new ArrayList();
for(int i=0; i&lt;_options.NumberOfPuzzles; i++)  
  {
puzzles.Add(GenerateOne());
  }
puzzles.Sort(new SolverResultsComparer(_options.Techniques));
return ((SolverResults)puzzles[puzzles.Count-1]).Puzzle;
}
</PRE>
<P>修改此代码，以使对 <B>GenerateOne</B> 的调用进入 <B>ThreadPool</B> 
中排队，并且您已获得对并行填数游戏生成的即时支持。当然，做此修改需要一些附加的同步，因为您需要延迟对 <B>puzzles.Sort</B> 
的调用，直至所有的后台线程完成对其填数游戏的生成并安全的将它们存储到集合中为止（对它的访问也需要进行同步）。您可以使用我在 MSDN Magazine 上的 
<A id=ctl00_MTCS_main_ctl11 
onclick="javascript:Track('ctl00_MTCS_main_ctl00|ctl00_MTCS_main_ctl11',this);" 
href="http://go.microsoft.com/fwlink/?LinkId=63627">my October 2004 .NET 
Matters</A>（英文）专栏中介绍的 <B>ThreadPoolWait</B> 类来完成此操作。</P>
<DIV><A 
href="http://msdn.microsoft.com/zh-cn/library/aa480686.aspx#mainSection"><IMG 
alt="" 
src="Microsoft Sudoku：优化 UMPC 应用程序的点触和手写操作.files/Aa480686.arrow_px_up(zh-cn,MSDN.10).gif" 
border=0> </A><A 
href="http://msdn.microsoft.com/zh-cn/library/aa480686.aspx#mainSection">返回页首</A> 
<BR></DIV>
<H3 id=EJAA>将填数游戏复制到剪贴板</H3>
<P>也许您要保存当前填数游戏的副本。进行此操作最简便的方法是将其文本表示复制到剪贴板。对此操作的支持已被内置于 <B>PuzzleState</B> 
中，由于其 <B>ToString</B> 覆盖返回填数游戏的文本表示；因此您需要做的就是将此字符串复制到剪贴板中。您只需将一小段代码添加到 
<B>MainForm</B> 的 <B>OnKeyDown</B> 覆盖中即可进行此操作。</P><PRE>if (e.KeyCode == Keys.C &amp;&amp; e.Control &amp;&amp; 
thePuzzleGrid.Visible &amp;&amp; thePuzzleGrid.Enabled)
{
if (!thePuzzleGrid.CollectingInk)
  {
string text = thePuzzleGrid.State.ToString();
try { Clipboard.SetDataObject(text, true); }  
catch(ExternalException){ }
  }
}
</PRE>
<DIV><A 
href="http://msdn.microsoft.com/zh-cn/library/aa480686.aspx#mainSection"><IMG 
alt="" 
src="Microsoft Sudoku：优化 UMPC 应用程序的点触和手写操作.files/Aa480686.arrow_px_up(zh-cn,MSDN.10).gif" 
border=0> </A><A 
href="http://msdn.microsoft.com/zh-cn/library/aa480686.aspx#mainSection">返回页首</A> 
<BR></DIV>
<H3 id=EGAA>其他操作...</H3>
<P>下列建议为您可以考虑进行的一些其他操作。 </P>
<UL>
  <LI>
  <P>实现其他算法来解决和生成游戏。 </P>
  <LI>
  <P>为玩家实现附加的可选提示，或修改当前的“建议单元格”提示以解释为什么建议选择该单元格。 </P>
  <LI>
  <P>支持附加的笔势。 </P>
  <LI>
  <P>允许填数游戏网格大于 9x9 </P>
  <LI>
  <P>添加 Windows Presentation Foundation (WPF) 用户界面。 </P>
  <LI>
  <P>使用 Windows Communication Foundation (WCF) 实现在网络上合作完成填数游戏。 </P>
  <LI>
  <P>为 Windows Vista 的 Windows Media Center 创建 Media Center Markup Language 
  (MCML) 界面。（请注意，启动此功能后，可通过使用箭头键导航，使用数字键输入值等远程控制来轻松自如地玩 Sudoku 实施程序了。） </P>
  <LI>
  <P>添加更多游戏相关的功能，例如高分跟踪以及游戏计时器。 </P></LI></UL>
<P>总之有无数种可能，我期待看到您自己的实现。来体会编写代码的乐趣吧！</P>
<DIV><A 
href="http://msdn.microsoft.com/zh-cn/library/aa480686.aspx#mainSection"><IMG 
alt="" 
src="Microsoft Sudoku：优化 UMPC 应用程序的点触和手写操作.files/Aa480686.arrow_px_up(zh-cn,MSDN.10).gif" 
border=0> </A><A 
href="http://msdn.microsoft.com/zh-cn/library/aa480686.aspx#mainSection">返回页首</A> 
<BR></DIV>
<H3 id=EDAA>致谢</H3>
<P>诚挚的感谢 Tablet PC 团队对本项目的支持；感谢 David Bessenhoffer 提供了完美的图像；感谢 Calvin Hsia 和 
Brian Goldfarb 提供的建议和意见；感谢 John Keefe 和 Luke Stein 介绍给我这样迷人的游戏；感谢 Eliot Graff 
帮助我完成此文档和发布的代码；感谢 Tamara Spiewak（对我以及 Sudoku）的关爱和支持。</P>
<DIV><A 
href="http://msdn.microsoft.com/zh-cn/library/aa480686.aspx#mainSection"><IMG 
alt="" 
src="Microsoft Sudoku：优化 UMPC 应用程序的点触和手写操作.files/Aa480686.arrow_px_up(zh-cn,MSDN.10).gif" 
border=0> </A><A 
href="http://msdn.microsoft.com/zh-cn/library/aa480686.aspx#mainSection">返回页首</A> 
<BR></DIV>
<H3 id=EAAA>作者简介</H3>
<P><B>Stephen Toub</B>是 Microsoft MSDN 团队的技术主管。他是 MSDN Magazine 的技术编辑，还是该杂志 .NET 
Matters 的专栏作家。Stephen 开发了 Sudoku 的实现，Sudoku 是 Touch Pack（所有 ultra-mobile PC 
附带的软件包）的一部分。</P>
<DIV><A 
href="http://msdn.microsoft.com/zh-cn/library/aa480686.aspx#mainSection"><IMG 
alt=返回页首 
src="Microsoft Sudoku：优化 UMPC 应用程序的点触和手写操作.files/Aa480686.arrow_px_up(zh-cn,MSDN.10).gif" 
border=0> </A><A 
href="http://msdn.microsoft.com/zh-cn/library/aa480686.aspx#mainSection">返回页首</A> 
</DIV></DIV></DIV></DIV></DIV></DIV></DIV>
<DIV style="DISPLAY: block; OVERFLOW: hidden; POSITION: relative">
<TABLE cellSpacing=0 cellPadding=0 width="100%" border=0>
  <TBODY>
  <TR>
    <TD class=MTPS_FooterFade id=ctl00_mtpsFooter_FooterFadeCell>
      <DIV class=MTPS_FooterLinks id=ctl00_mtpsFooter_SiteLinks><A 
      id=ctl00_mtpsFooter_LocalFooterLink0 title=个人信息中心 
      href="http://go.microsoft.com/?linkid=8786242&amp;lcid=2052">个人信息中心</A>&nbsp;|&nbsp;<A 
      id=ctl00_mtpsFooter_LocalFooterLink1 title=法律信息 
      href="http://www.microsoft.com/legal/">法律信息</A>&nbsp;|&nbsp;<A 
      id=ctl00_mtpsFooter_LocalFooterLink2 title="MSDN Flash 中心" 
      href="http://www.microsoft.com/china/msdn/newsletter/default.aspx">MSDN 
      Flash 中心</A>&nbsp;|&nbsp;<A id=ctl00_mtpsFooter_LocalFooterLink3 
      title=联系我们 href="mailto:msdnonln@microsoft.com">联系我们</A> </DIV><SPAN 
      class=MTPS_FooterCopyright id=ctl00_mtpsFooter_MSFT_copyright 
      title="© 2009  Microsoft Corporation 版权所有。 ">© 2009 Microsoft Corporation 
      版权所有。 </SPAN><A class=MTPS_FooterLinks id=ctl00_mtpsFooter_MSFT_Terms 
      title=保留所有权利 
      href="http://msdn.microsoft.com/zh-cn/cc300389.aspx">保留所有权利</A> <SPAN 
      id=ctl00_mtpsFooter_PipeSpan1>|</SPAN> <A class=MTPS_FooterLinks 
      id=ctl00_mtpsFooter_MSFT_Trademarks title=商标 
      href="http://www.microsoft.com/library/toolbar/3.0/trademarks/zh-cn.mspx">商标</A> 
      <SPAN id=ctl00_mtpsFooter_PipeSpan2>|</SPAN> <A class=MTPS_FooterLinks 
      id=ctl00_mtpsFooter_MSFT_PrivacyStatement title=隐私权声明 
      href="http://www.microsoft.com/info/cn/privacy.mspx">隐私权声明</A> <A 
      class=MTPS_FooterLinks id=ctl00_mtpsFooter_MSFT_Feedback target=_blank 
      rel=sitefeedback></A></TD>
    <TD id=ctl00_mtpsFooter_FooterLogoCell><A id=ctl00_mtpsFooter_MSFT_LOGO 
      title="Microsoft Corporation" 
      href="http://www.microsoft.com/zh/cn/default.aspx"><IMG 
      class=msdn_TabC_mslogo id=ctl00_mtpsFooter_MSFT_imgLogo 
      style="BORDER-TOP-WIDTH: 0px; BORDER-LEFT-WIDTH: 0px; BORDER-BOTTOM-WIDTH: 0px; BORDER-RIGHT-WIDTH: 0px" 
      src="Microsoft Sudoku：优化 UMPC 应用程序的点触和手写操作.files/clear.gif"></A> 
  </TD></TR></TBODY></TABLE></DIV></DIV></DIV></DIV>
<DIV style="DISPLAY: none"><IMG height=0 alt="Page view tracker" hspace=0 
src="Microsoft Sudoku：优化 UMPC 应用程序的点触和手写操作.files/trans_pixel.gif" width=0 
border=0></DIV>
<SCRIPT 
src="Microsoft Sudoku：优化 UMPC 应用程序的点触和手写操作.files/webtrendsscript-bn20091211.js" 
type=text/javascript></SCRIPT>
<NOSCRIPT>
<DIV><IMG id=Img1 height=1 alt=DCSIMG 
src="Microsoft Sudoku：优化 UMPC 应用程序的点触和手写操作.files/njs.gif" 
width=1></DIV></NOSCRIPT>
<SCRIPT src="Microsoft Sudoku：优化 UMPC 应用程序的点触和手写操作.files/CAQB07DY.js" 
type=text/javascript></SCRIPT>

<SCRIPT type=text/javascript>
        //<![CDATA[
        setTimeout(function() {
            var hashElementId = document.location.hash.substr(1);
            var hashElement = document.getElementById(hashElementId);
            if (hashElement != null) {
                hashElement.scrollIntoView(true);
            }
        }, 1);
        //]]>
    </SCRIPT>

<SCRIPT type=text/javascript>
//<![CDATA[
 RegisterAutoSubmit('ctl00_ib1_Flyout_Rating1','ctl00_ib1_Flyout_btnSend','contentRatingaa480686MSDN.10zh-cn');

//]]>
</SCRIPT>

<SCRIPT language=javascript type=text/javascript>
<!--
 function GetContentWnd(){ return parent; } function OnInitPage(){var oWnd = GetContentWnd();} function OnSave(){ var oWnd = GetContentWnd(); window.external.addFavorite('http://msdn.microsoft.com/zh-cn/library/aa480686.aspx', oWnd.document.title ); }

-->
</SCRIPT>

<SCRIPT type=text/javascript>
//<![CDATA[
document.getElementById('ctl00_ib1_Flyout_rtgContainer').title="请单击星形符号并提供反馈";
Sys.Application.initialize();
TFly_Init('ctl00_ib1_Flyout',5,2,1,0,0,'','',0,400,'','TFlyPreAnimate','','');
MTPS.Controls.MtpsRating.createMtpsRating('ctl00_ib1_Flyout_Rating1','ratingStar','filledRatingStar','emptyRatingStar','savedRatingStar','5','LeftToRightTopToBottom', false);MTPS.Controls.ResizeableArea = new MTPS.Controls.CreateResizeableArea('ctl00_LibFrame','ctl00_raSplitter','ctl00_raLeft','ctl00_raRight','tocwidth','toccollapsed','173');var tocVectorObject;var hostbase;var closedImg;var openImg;var emptyImg;var loadingImg;var tocVector;var tocIndex = 0;var holder;var tocLoaded = false;var tocCollapsed = 'False';tocVectorObject = {"nodes": [{"id": "ms310241(n)" ,"url": "/ms310241(n)"},{"id": "bb814937(n)" ,"url": "/bb814937(n)"},{"id": "bb507200(n)" ,"url": "/bb507200(n)"},{"id": "aa480686(n)" ,"url": "/aa480686(n)"}]};hostbase ="http://msdn.microsoft.com/zh-cn/library";tocVector = eval(tocVectorObject);closedImg = "LibC_c";openImg = "LibC_o";emptyImg = "LibC_e";var tocDir = false;TFly_Init('ctl00_eb1_ctl00',0,-1,-1,0,0,'statHover','',0,0,'EyeBrowMenuBarSetHeight','ExpEye','','');
TFly_Init('ctl00_eb1_ctl01',0,-1,-1,0,0,'statHover','',0,0,'EyeBrowMenuBarSetHeight','ExpEye','','');
TFly_Init('ctl00_eb1_ctl02',0,-1,-1,0,0,'statHover','',0,0,'EyeBrowMenuBarSetHeight','ExpEye','','');
TFly_Init('ctl00_eb1_ctl03',0,-1,-1,0,0,'statHover','',0,0,'EyeBrowMenuBarSetHeight','ExpEye','','');
//]]>
</SCRIPT>
</FORM>
<SCRIPT language=javascript 
src="Microsoft Sudoku：优化 UMPC 应用程序的点触和手写操作.files/broker.js" 
type=text/javascript></SCRIPT>
</BODY></HTML>
